jskelet 0.6.7 → 0.6.9
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 +13 -0
- package/docs/07-yapilandirma.md +59 -10
- package/docs/08-build.md +22 -1
- package/docs/10-dagitim.md +22 -1
- package/docs/en/07-configuration.md +65 -13
- package/docs/en/08-build.md +23 -3
- package/docs/en/10-deployment.md +22 -1
- package/package.json +1 -1
- package/src/build/build.mjs +12 -3
- package/src/build/tasks/assets-upload.mjs +190 -0
- package/src/config/defaults.js +20 -1
- package/src/config/index.js +150 -29
- package/src/index.js +7 -1
- package/src/server/assets.js +57 -6
- package/src/server/logs/pipeline.js +1 -1
- package/src/server/logs/s3-put.js +10 -6
- package/types/config/defaults.d.ts +20 -1
- package/types/config/index.d.ts +31 -2
- package/types/index.d.ts +1 -1
- package/types/server/assets.d.ts +8 -0
- package/types/server/logs/s3-put.d.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,19 @@ one is listed under a **Breaking** heading.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Optional CDN asset publishing: `build.assets.prefix` /
|
|
14
|
+
`JSKELET_ASSET_PREFIX` prefixes `asset()` and optimised image URLs (manifest
|
|
15
|
+
stays relative on disk). When `JSKELET_ASSETS_BUCKET` (or
|
|
16
|
+
`build.assets.upload`) is set, production `jskelet build` PutObjects
|
|
17
|
+
`public/assets/` (and optional `include` dirs) to S3 after precompress.
|
|
18
|
+
Credentials prefer standard `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` /
|
|
19
|
+
`AWS_SESSION_TOKEN`, then `JSKELET_S3_*`. The Assets upload step always
|
|
20
|
+
appears on production builds; when off it logs a one-line reason
|
|
21
|
+
(`skipped (no JSKELET_ASSETS_BUCKET)`, `skipped (credentials missing)`, …).
|
|
22
|
+
Watch mode never uploads. `.br` / `.gz` siblings are skipped.
|
|
23
|
+
|
|
11
24
|
### Changed
|
|
12
25
|
|
|
13
26
|
- Shared cache entries of 1 KB or more are stored as brotli (`JSK\x01`
|
package/docs/07-yapilandirma.md
CHANGED
|
@@ -95,6 +95,13 @@ export default {
|
|
|
95
95
|
client: {
|
|
96
96
|
groups: { charts: ["chart.js"] },
|
|
97
97
|
},
|
|
98
|
+
assets: {
|
|
99
|
+
prefix: process.env.JSKELET_ASSET_PREFIX,
|
|
100
|
+
upload: {
|
|
101
|
+
enabled: Boolean(process.env.JSKELET_ASSETS_BUCKET),
|
|
102
|
+
bucket: process.env.JSKELET_ASSETS_BUCKET,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
98
105
|
},
|
|
99
106
|
|
|
100
107
|
async headers() {
|
|
@@ -592,11 +599,14 @@ Elle URL kurmak için `remoteImageUrl(src, { width })` (`jskelet`).
|
|
|
592
599
|
|
|
593
600
|
## `build`
|
|
594
601
|
|
|
595
|
-
**Tip:** `{ client?: { splitting?, groups?, chunkGroup? } }`
|
|
596
|
-
|
|
602
|
+
**Tip:** `{ client?: { splitting?, groups?, chunkGroup? }, assets?: { prefix?, upload? } }`
|
|
603
|
+
— **Varsayılan:**
|
|
604
|
+
`{ client: { splitting: true, groups: {}, chunkGroup: null }, assets: { prefix: "", upload: { enabled: false, … } } }`
|
|
605
|
+
|
|
606
|
+
Client JS (esbuild) bundling knobs ve opsiyonel CDN / S3 asset dağıtımı.
|
|
607
|
+
`fonts` / `icons` / `images` / `clientEnv` hâlâ top-level kalır.
|
|
597
608
|
|
|
598
|
-
|
|
599
|
-
hâlâ top-level kalır; bu bölüm yalnızca splitting ve chunk gruplaması içindir.
|
|
609
|
+
### `build.client`
|
|
600
610
|
|
|
601
611
|
| Alan | Tip | Varsayılan | Anlamı |
|
|
602
612
|
| --- | --- | --- | --- |
|
|
@@ -611,6 +621,29 @@ zayıflar — bir üyeyi kullanan sayfa grubun geri kalanını da indirebilir
|
|
|
611
621
|
`chunks/chunk.<hash>.js` kalır; grup adı config anahtarıdır, dosya adı değil.
|
|
612
622
|
Ayrıntı: [08-build.md](./08-build.md).
|
|
613
623
|
|
|
624
|
+
### `build.assets`
|
|
625
|
+
|
|
626
|
+
CDN origin ve production build sonunda S3 upload. **Kapalıyken hiçbir şey
|
|
627
|
+
değişmez** — asset'ler origin'den servis edilir.
|
|
628
|
+
|
|
629
|
+
| Alan | Tip | Varsayılan | Anlamı |
|
|
630
|
+
| --- | --- | --- | --- |
|
|
631
|
+
| `assets.prefix` | `string` | `""` | `asset()` / optimize görsel URL öneki (sonunda `/` yok). `JSKELET_ASSET_PREFIX` ezer. Manifest diskte göreli kalır. |
|
|
632
|
+
| `assets.upload.enabled` | `boolean` | `false` | `true` veya `JSKELET_ASSETS_BUCKET` doluysa açılır; açık `false` ezer. |
|
|
633
|
+
| `assets.upload.bucket` | `string \| null` | `null` | Bucket veya `bucket/key/prefix`. `JSKELET_ASSETS_BUCKET` ezer. |
|
|
634
|
+
| `assets.upload.keyPrefix` | `string` | `""` | Yolda verilmediyse nesne anahtarı öneki (`prefix` takma adı da kabul). |
|
|
635
|
+
| `assets.upload.region` | `string` | `us-east-1` | `AWS_REGION` / `AWS_DEFAULT_REGION` / `JSKELET_S3_REGION`; custom endpoint'te varsayılan `auto`. |
|
|
636
|
+
| `assets.upload.endpoint` | `string \| null` | `null` | S3-uyumlu API; `AWS_ENDPOINT_URL` / `JSKELET_S3_API_URL` ezer. |
|
|
637
|
+
| `assets.upload.include` | `string[]` | `["assets"]` | `public/` altı dizinler (`assets`, isteğe `fonts`, …). |
|
|
638
|
+
| `assets.upload.cacheControl` | `string` | `public, max-age=31536000, immutable` | PutObject `Cache-Control`. |
|
|
639
|
+
|
|
640
|
+
Credential config'e yazılmaz. Öncelik: `AWS_ACCESS_KEY_ID` /
|
|
641
|
+
`AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN`, yoksa `JSKELET_S3_*`. Production
|
|
642
|
+
build'de Assets upload adımı her zaman görünür; kapalıysa
|
|
643
|
+
`skipped (no JSKELET_ASSETS_BUCKET)` / `skipped (credentials missing)` gibi tek
|
|
644
|
+
satır sebep basılır (build düşmez). Watch turunda adım yok. `.br` / `.gz`
|
|
645
|
+
yüklenmez. Ayrıntı: [08-build.md](./08-build.md), [10-dagitim.md](./10-dagitim.md).
|
|
646
|
+
|
|
614
647
|
```js
|
|
615
648
|
build: {
|
|
616
649
|
client: {
|
|
@@ -623,6 +656,14 @@ build: {
|
|
|
623
656
|
if (id.includes("node_modules/lodash")) return "vendor";
|
|
624
657
|
},
|
|
625
658
|
},
|
|
659
|
+
assets: {
|
|
660
|
+
prefix: process.env.JSKELET_ASSET_PREFIX, // https://dxxxx.cloudfront.net
|
|
661
|
+
upload: {
|
|
662
|
+
enabled: Boolean(process.env.JSKELET_ASSETS_BUCKET),
|
|
663
|
+
bucket: process.env.JSKELET_ASSETS_BUCKET, // my-bucket veya my-bucket/static
|
|
664
|
+
include: ["assets", "fonts"],
|
|
665
|
+
},
|
|
666
|
+
},
|
|
626
667
|
},
|
|
627
668
|
```
|
|
628
669
|
|
|
@@ -954,7 +995,8 @@ parça silinir.
|
|
|
954
995
|
| `s3.flushIntervalMs` | `number` | `5000` | Batch flush aralığı |
|
|
955
996
|
| `s3.maxBatch` | `number` | `100` | Bu kadar satırda erken flush |
|
|
956
997
|
|
|
957
|
-
S3 credential'ları config'e yazılmaz: `
|
|
998
|
+
S3 credential'ları config'e yazılmaz. Öncelik: `AWS_ACCESS_KEY_ID` /
|
|
999
|
+
`AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN`, yoksa `JSKELET_S3_ACCESS_KEY_ID`,
|
|
958
1000
|
`JSKELET_S3_SECRET_ACCESS_KEY`, isteğe bağlı `JSKELET_S3_SESSION_TOKEN`.
|
|
959
1001
|
Bucket/region/credential eksikse uyarı basılır ve S3 sink kapanır; site ayağa
|
|
960
1002
|
kalkmaya devam eder. Framework `@aws-sdk` taşımaz — PutObject SigV4 ile
|
|
@@ -1186,14 +1228,21 @@ basılmaz.
|
|
|
1186
1228
|
| `DEV_GATE` | `devGate` | kapalı | `1` gate'i açar, `0` config'te açık olsa da kapatır. `DEV_TOKEN` tek başına açmaz. [09](./09-dev-araclari.md) |
|
|
1187
1229
|
| `DEV_TOKEN` | `devGate`, `prewarm` | — | Gate açıkken beklenen sır. Yoksa veya gate kapalıysa site açık kalır. Isıtma, gate açıkken token'ı çerez olarak taşır. [09](./09-dev-araclari.md) |
|
|
1188
1230
|
| `JSKELET_ADMIN` | `createApp` | — | Ayarlıysa yönetim panelini açar; `0` config'te açık olan paneli kapatır. Env config'i ezer, çünkü panel genelde bir arıza sırasında tek seferlik açılır. [06](./06-cache.md) |
|
|
1231
|
+
| `JSKELET_ASSET_PREFIX` | `build.assets.prefix` | — | CDN origin; `asset()` URL'lerine eklenir (sonunda `/` yok) |
|
|
1232
|
+
| `JSKELET_ASSETS_BUCKET` | `build.assets.upload` | — | Asset upload hedefi: bucket veya `bucket/prefix`. Credential ile birlikte production build sonunda PutObject |
|
|
1233
|
+
| `AWS_ACCESS_KEY_ID` | S3 PutObject | — | Standart AWS imza anahtarı (log + asset upload); `JSKELET_S3_*`'dan önce |
|
|
1234
|
+
| `AWS_SECRET_ACCESS_KEY` | S3 PutObject | — | Standart AWS imza sırrı |
|
|
1235
|
+
| `AWS_SESSION_TOKEN` | S3 PutObject | — | Geçici credential (OIDC / STS) |
|
|
1236
|
+
| `AWS_REGION` / `AWS_DEFAULT_REGION` | `build.assets.upload` | — | Asset upload bölgesi |
|
|
1237
|
+
| `AWS_ENDPOINT_URL` | `build.assets.upload` | — | S3-uyumlu endpoint (asset upload) |
|
|
1189
1238
|
| `JSKELET_LOG_BUCKET` | `logs.s3` | — | Log hedefi: bucket ya da `bucket/prefix` yolu. Credential ile birlikte varsa sink otomatik açılır |
|
|
1190
1239
|
| `JSKELET_S3_BUCKET` | `logs.s3` | — | `JSKELET_LOG_BUCKET` yoksa bucket; `JSKELET_S3_KEY_PREFIX` ile birleşir |
|
|
1191
1240
|
| `JSKELET_S3_KEY_PREFIX` | `logs.s3` | — | `JSKELET_S3_BUCKET` ile kullanılır (`bucket/prefix`) |
|
|
1192
|
-
| `JSKELET_S3_ACCESS_KEY_ID` |
|
|
1193
|
-
| `JSKELET_S3_SECRET_ACCESS_KEY` |
|
|
1194
|
-
| `JSKELET_S3_SESSION_TOKEN` |
|
|
1195
|
-
| `JSKELET_S3_REGION` | `logs.s3` | `auto` |
|
|
1196
|
-
| `JSKELET_S3_API_URL` | `logs.s3` | — | S3-uyumlu endpoint
|
|
1241
|
+
| `JSKELET_S3_ACCESS_KEY_ID` | S3 PutObject | — | `AWS_*` yoksa PutObject imzası (log + asset) |
|
|
1242
|
+
| `JSKELET_S3_SECRET_ACCESS_KEY` | S3 PutObject | — | İmza sırrı (`JSKELET_S3_ACCESS_SECRET` yedek ad) |
|
|
1243
|
+
| `JSKELET_S3_SESSION_TOKEN` | S3 PutObject | — | Geçici credential için isteğe bağlı |
|
|
1244
|
+
| `JSKELET_S3_REGION` | `logs.s3` / asset upload | `auto` (log) | Log'da verilmezse `auto`; asset upload'ta `AWS_REGION` yoksa |
|
|
1245
|
+
| `JSKELET_S3_API_URL` | `logs.s3` / asset upload | — | S3-uyumlu endpoint |
|
|
1197
1246
|
| `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) |
|
|
1198
1247
|
| `JSKELET_CLOUDFLARE_ZONE_ID` | Cloudflare cache yüzeyi | — | Zone kimliği. Token'la birlikte verilmedikçe hiçbir Cloudflare ucu çağrılmaz |
|
|
1199
1248
|
| `JSKELET_CLOUDFLARE_HOSTNAME` | Cloudflare cache yüzeyi | — | Purge URL'lerinin kökü. Panel iç bir adresten açılıyorsa gerekir |
|
package/docs/08-build.md
CHANGED
|
@@ -20,6 +20,7 @@ davranışı burada. Çıktının çalışma anında nasıl servis edildiği
|
|
|
20
20
|
5. Images config.images !== false, watch değil ve sharp kurulu ise
|
|
21
21
|
6. Manifest .jskelet/manifest.json
|
|
22
22
|
7. Precompress watch değilse
|
|
23
|
+
8. Assets upload watch değilse (kapalıysa `skipped (sebep)`)
|
|
23
24
|
```
|
|
24
25
|
|
|
25
26
|
Şablon derlemesi asset taramasından **önce** biter; istek yolunda parse yoktur.
|
|
@@ -30,8 +31,11 @@ Sıra rastgele değil:
|
|
|
30
31
|
|
|
31
32
|
- **CSS ikon sprite'ından sonra gelir.** Sprite bir varlıktır ve sınıf üretmez,
|
|
32
33
|
ama manifest anahtarı verir.
|
|
33
|
-
- **Precompress
|
|
34
|
+
- **Precompress, upload'tan hemen önce:** origin hâlâ `.br`/`.gz` üretir; S3
|
|
35
|
+
adımı yalnızca ham dosyaları kopyalar.
|
|
34
36
|
- **Görseller watch turunda hiç çalışmaz:** `sharp` ile yeniden kodlama pahalı.
|
|
37
|
+
- **Assets upload watch'ta yok:** yalnızca production `jskelet build`; kapalıysa
|
|
38
|
+
yine satır basılır (`skipped (no JSKELET_ASSETS_BUCKET)` vb.).
|
|
35
39
|
|
|
36
40
|
Görevler yalnızca ilgili yapılandırma varsa çalışır. Font tanımlamayan bir proje
|
|
37
41
|
font adımını hiç görmez; bu, "framework her projeye kendi varsayımlarını
|
|
@@ -74,8 +78,25 @@ dosya adlarını okunur tutuyor. Hash'li olmaları sayesinde bu dosyalara
|
|
|
74
78
|
```
|
|
75
79
|
|
|
76
80
|
- `asset(name)` manifest'te varsa hash'li URL'i, yoksa `/assets/<name>` döner.
|
|
81
|
+
`build.assets.prefix` / `JSKELET_ASSET_PREFIX` doluysa bu göreli yolun önüne
|
|
82
|
+
CDN origin eklenir (manifest diskte göreli kalır).
|
|
77
83
|
- `hasAsset(name)` manifest'te olup olmadığını söyler.
|
|
78
84
|
|
|
85
|
+
### CDN prefix ve S3 upload
|
|
86
|
+
|
|
87
|
+
Varsayılan kapalıdır. Açılınca:
|
|
88
|
+
|
|
89
|
+
1. Production build `public/assets/` (ve `include` listesindeki diğer dizinler)
|
|
90
|
+
altındaki dosyaları S3'e PutObject ile kopyalar.
|
|
91
|
+
2. Çalışma anında `asset()` / optimize görseller `JSKELET_ASSET_PREFIX` ile
|
|
92
|
+
absolute CDN URL basar. Entry CDN'deyse esbuild chunk'ları göreli yolla aynı
|
|
93
|
+
host'a gider.
|
|
94
|
+
|
|
95
|
+
Credential: `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` (önce), yoksa
|
|
96
|
+
`JSKELET_S3_*`. Bucket: `JSKELET_ASSETS_BUCKET`. Config sözlüğü:
|
|
97
|
+
[07-yapilandirma.md](./07-yapilandirma.md#build). Dağıtım notları:
|
|
98
|
+
[10-dagitim.md](./10-dagitim.md).
|
|
99
|
+
|
|
79
100
|
Build çalışmadıysa uygulama yine ayağa kalkar: `hasAsset()` false olur, layout
|
|
80
101
|
stylesheet ve script etiketlerini hiç basmaz. `jskelet build` unutulduğunda hata
|
|
81
102
|
yerine stilsiz ama çalışan bir sayfa görürsünüz. Manifest hiç yoksa bir kez uyarı
|
package/docs/10-dagitim.md
CHANGED
|
@@ -51,7 +51,28 @@ ayarlamayı düşünmeniz gerekenler:
|
|
|
51
51
|
| `PREWARM_MAX` | Site boyutuna göre | Açılışta ısıtılacak sayfa sayısı |
|
|
52
52
|
| `PREWARM_INTERVAL_SECONDS` | `0` ya da uzun bir değer | Hiç ziyaret edilmeyen sayfaları sıcak tutmak isterseniz |
|
|
53
53
|
| `DEV_GATE` + `DEV_TOKEN` | Yalnızca staging'de | Yayına açılmamış ortamı gizler. Token tek başına siteyi kilitlemez |
|
|
54
|
-
| `JSKELET_S3_*` | Access log'
|
|
54
|
+
| `JSKELET_S3_*` / `AWS_*` | Access log veya asset'leri S3'e yazıyorsanız | Credential; AWS deploy'da `AWS_ACCESS_KEY_ID` vb. yeter. Ayrıntı [07](./07-yapilandirma.md) |
|
|
55
|
+
| `JSKELET_ASSETS_BUCKET` | Hash'li asset'leri S3/CloudFront'a basıyorsanız | Production `jskelet build` sonunda PutObject; `JSKELET_ASSET_PREFIX` ile HTML CDN URL basar |
|
|
56
|
+
|
|
57
|
+
### Asset'leri S3 / CloudFront'a basmak
|
|
58
|
+
|
|
59
|
+
Varsayılan kapalıdır. CI veya prod build ortamında:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
JSKELET_ASSETS_BUCKET=my-app-assets # veya my-app-assets/static
|
|
63
|
+
JSKELET_ASSET_PREFIX=https://dxxxx.cloudfront.net
|
|
64
|
+
AWS_REGION=eu-central-1
|
|
65
|
+
AWS_ACCESS_KEY_ID=…
|
|
66
|
+
AWS_SECRET_ACCESS_KEY=…
|
|
67
|
+
# geçici credential ise:
|
|
68
|
+
# AWS_SESSION_TOKEN=…
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`jskelet build` bitince `public/assets/` (ve config'te `include` edilen
|
|
72
|
+
dizinler) S3'e kopyalanır; origin'deki dosyalar da durur. Bucket veya
|
|
73
|
+
credential yoksa adım `skipped (…)` ile sebep basar, build devam eder. Watch /
|
|
74
|
+
`jskelet dev` upload yapmaz. CSP kullanıyorsanız CDN origin'ini `script-src` /
|
|
75
|
+
`style-src` / `img-src` ve `preconnect` listesine ekleyin.
|
|
55
76
|
|
|
56
77
|
Production'da dosya veya S3 sink açıldığında HTTP access log middleware
|
|
57
78
|
otomatik mount edilir (`logs.kinds` içinde `http` varsa). Admin paneli ring'i
|
|
@@ -99,6 +99,13 @@ export default {
|
|
|
99
99
|
client: {
|
|
100
100
|
groups: { charts: ["chart.js"] },
|
|
101
101
|
},
|
|
102
|
+
assets: {
|
|
103
|
+
prefix: process.env.JSKELET_ASSET_PREFIX,
|
|
104
|
+
upload: {
|
|
105
|
+
enabled: Boolean(process.env.JSKELET_ASSETS_BUCKET),
|
|
106
|
+
bucket: process.env.JSKELET_ASSETS_BUCKET,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
102
109
|
},
|
|
103
110
|
|
|
104
111
|
async headers() {
|
|
@@ -605,11 +612,14 @@ hand, use `remoteImageUrl(src, { width })` from `jskelet`.
|
|
|
605
612
|
|
|
606
613
|
## `build`
|
|
607
614
|
|
|
608
|
-
**Type:** `{ client?: { splitting?, groups?, chunkGroup? } }`
|
|
609
|
-
|
|
615
|
+
**Type:** `{ client?: { splitting?, groups?, chunkGroup? }, assets?: { prefix?, upload? } }`
|
|
616
|
+
— **Default:**
|
|
617
|
+
`{ client: { splitting: true, groups: {}, chunkGroup: null }, assets: { prefix: "", upload: { enabled: false, … } } }`
|
|
618
|
+
|
|
619
|
+
Client JS (esbuild) bundling knobs and optional CDN / S3 asset publishing.
|
|
620
|
+
`fonts` / `icons` / `images` / `clientEnv` stay top-level.
|
|
610
621
|
|
|
611
|
-
|
|
612
|
-
stay top-level; this section is only for splitting and chunk grouping.
|
|
622
|
+
### `build.client`
|
|
613
623
|
|
|
614
624
|
| Field | Type | Default | Meaning |
|
|
615
625
|
| --- | --- | --- | --- |
|
|
@@ -624,6 +634,31 @@ group weakens — a page that uses one member may download the rest of the group
|
|
|
624
634
|
`chunks/chunk.<hash>.js`; the group name is a config key, not the file name.
|
|
625
635
|
Details: [08-build.md](./08-build.md).
|
|
626
636
|
|
|
637
|
+
### `build.assets`
|
|
638
|
+
|
|
639
|
+
CDN origin and S3 upload at the end of a production build. **When off, nothing
|
|
640
|
+
changes** — assets stay on the origin.
|
|
641
|
+
|
|
642
|
+
| Field | Type | Default | Meaning |
|
|
643
|
+
| --- | --- | --- | --- |
|
|
644
|
+
| `assets.prefix` | `string` | `""` | Prefix for `asset()` / optimised image URLs (no trailing `/`). `JSKELET_ASSET_PREFIX` overrides. The on-disk manifest stays relative. |
|
|
645
|
+
| `assets.upload.enabled` | `boolean` | `false` | Turns on when `true` or when `JSKELET_ASSETS_BUCKET` is set; explicit `false` wins. |
|
|
646
|
+
| `assets.upload.bucket` | `string \| null` | `null` | Bucket or `bucket/key/prefix`. `JSKELET_ASSETS_BUCKET` overrides. |
|
|
647
|
+
| `assets.upload.keyPrefix` | `string` | `""` | Object key prefix when not given in the path (`prefix` is accepted as an alias). |
|
|
648
|
+
| `assets.upload.region` | `string` | `us-east-1` | `AWS_REGION` / `AWS_DEFAULT_REGION` / `JSKELET_S3_REGION`; defaults to `auto` with a custom endpoint. |
|
|
649
|
+
| `assets.upload.endpoint` | `string \| null` | `null` | S3-compatible API; `AWS_ENDPOINT_URL` / `JSKELET_S3_API_URL` overrides. |
|
|
650
|
+
| `assets.upload.include` | `string[]` | `["assets"]` | Directories under `public/` (`assets`, optionally `fonts`, …). |
|
|
651
|
+
| `assets.upload.cacheControl` | `string` | `public, max-age=31536000, immutable` | PutObject `Cache-Control`. |
|
|
652
|
+
|
|
653
|
+
Credentials are not written in the config. Preference order:
|
|
654
|
+
`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN`, then
|
|
655
|
+
`JSKELET_S3_*`. On a production build the Assets upload step always appears;
|
|
656
|
+
when off it logs a one-line reason such as
|
|
657
|
+
`skipped (no JSKELET_ASSETS_BUCKET)` or `skipped (credentials missing)` (the
|
|
658
|
+
build does not fail). No upload on watch passes. `.br` / `.gz` siblings are not
|
|
659
|
+
uploaded. Details: [08-build.md](./08-build.md),
|
|
660
|
+
[10-deployment.md](./10-deployment.md).
|
|
661
|
+
|
|
627
662
|
```js
|
|
628
663
|
build: {
|
|
629
664
|
client: {
|
|
@@ -636,6 +671,14 @@ build: {
|
|
|
636
671
|
if (id.includes("node_modules/lodash")) return "vendor";
|
|
637
672
|
},
|
|
638
673
|
},
|
|
674
|
+
assets: {
|
|
675
|
+
prefix: process.env.JSKELET_ASSET_PREFIX, // https://dxxxx.cloudfront.net
|
|
676
|
+
upload: {
|
|
677
|
+
enabled: Boolean(process.env.JSKELET_ASSETS_BUCKET),
|
|
678
|
+
bucket: process.env.JSKELET_ASSETS_BUCKET, // my-bucket or my-bucket/static
|
|
679
|
+
include: ["assets", "fonts"],
|
|
680
|
+
},
|
|
681
|
+
},
|
|
639
682
|
},
|
|
640
683
|
```
|
|
641
684
|
|
|
@@ -971,10 +1014,12 @@ deleted.
|
|
|
971
1014
|
| `s3.flushIntervalMs` | `number` | `5000` | Batch flush interval |
|
|
972
1015
|
| `s3.maxBatch` | `number` | `100` | Flush early after this many lines |
|
|
973
1016
|
|
|
974
|
-
S3 credentials are not written in the config:
|
|
975
|
-
`
|
|
976
|
-
|
|
977
|
-
|
|
1017
|
+
S3 credentials are not written in the config. Preference order:
|
|
1018
|
+
`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN`, then
|
|
1019
|
+
`JSKELET_S3_ACCESS_KEY_ID`, `JSKELET_S3_SECRET_ACCESS_KEY`, optional
|
|
1020
|
+
`JSKELET_S3_SESSION_TOKEN`. Missing bucket/region/credentials warn and disable
|
|
1021
|
+
the S3 sink; the site still starts. The framework does not ship `@aws-sdk` —
|
|
1022
|
+
PutObject is embedded with SigV4.
|
|
978
1023
|
|
|
979
1024
|
```js
|
|
980
1025
|
logs: {
|
|
@@ -1207,14 +1252,21 @@ and no warning is printed.
|
|
|
1207
1252
|
| `DEV_GATE` | `devGate` | off | `1` turns the gate on, `0` turns it off even when config enabled it. `DEV_TOKEN` alone does not turn it on. [09](./09-dev-tools.md) |
|
|
1208
1253
|
| `DEV_TOKEN` | `devGate`, `prewarm` | — | The secret expected while the gate is on. If it is missing, or the gate is off, the site stays public. Prewarming carries the token as a cookie only while the gate is on. [09](./09-dev-tools.md) |
|
|
1209
1254
|
| `JSKELET_ADMIN` | `createApp` | — | When set, turns the admin panel on; `0` turns off a panel enabled in the config. The env wins because the panel is usually opened once during an incident. [06](./06-caching.md) |
|
|
1255
|
+
| `JSKELET_ASSET_PREFIX` | `build.assets.prefix` | — | CDN origin prepended to `asset()` URLs (no trailing `/`) |
|
|
1256
|
+
| `JSKELET_ASSETS_BUCKET` | `build.assets.upload` | — | Asset upload target: bucket or `bucket/prefix`. With credentials, production build PutObject |
|
|
1257
|
+
| `AWS_ACCESS_KEY_ID` | S3 PutObject | — | Standard AWS signing key (logs + asset upload); before `JSKELET_S3_*` |
|
|
1258
|
+
| `AWS_SECRET_ACCESS_KEY` | S3 PutObject | — | Standard AWS signing secret |
|
|
1259
|
+
| `AWS_SESSION_TOKEN` | S3 PutObject | — | Temporary credentials (OIDC / STS) |
|
|
1260
|
+
| `AWS_REGION` / `AWS_DEFAULT_REGION` | `build.assets.upload` | — | Asset upload region |
|
|
1261
|
+
| `AWS_ENDPOINT_URL` | `build.assets.upload` | — | S3-compatible endpoint (asset upload) |
|
|
1210
1262
|
| `JSKELET_LOG_BUCKET` | `logs.s3` | — | Log target: bucket or `bucket/prefix` path. With credentials, the sink turns on automatically |
|
|
1211
1263
|
| `JSKELET_S3_BUCKET` | `logs.s3` | — | Bucket when `JSKELET_LOG_BUCKET` is unset; joins with `JSKELET_S3_KEY_PREFIX` |
|
|
1212
1264
|
| `JSKELET_S3_KEY_PREFIX` | `logs.s3` | — | Used with `JSKELET_S3_BUCKET` (`bucket/prefix`) |
|
|
1213
|
-
| `JSKELET_S3_ACCESS_KEY_ID` |
|
|
1214
|
-
| `JSKELET_S3_SECRET_ACCESS_KEY` |
|
|
1215
|
-
| `JSKELET_S3_SESSION_TOKEN` |
|
|
1216
|
-
| `JSKELET_S3_REGION` | `logs.s3` | `auto` |
|
|
1217
|
-
| `JSKELET_S3_API_URL` | `logs.s3` | — | S3-compatible endpoint
|
|
1265
|
+
| `JSKELET_S3_ACCESS_KEY_ID` | S3 PutObject | — | Signs PutObject when `AWS_*` is unset (logs + assets) |
|
|
1266
|
+
| `JSKELET_S3_SECRET_ACCESS_KEY` | S3 PutObject | — | Signing secret (`JSKELET_S3_ACCESS_SECRET` is an alias) |
|
|
1267
|
+
| `JSKELET_S3_SESSION_TOKEN` | S3 PutObject | — | Optional, for temporary credentials |
|
|
1268
|
+
| `JSKELET_S3_REGION` | `logs.s3` / asset upload | `auto` (logs) | Logs default to `auto`; asset upload uses it when `AWS_REGION` is unset |
|
|
1269
|
+
| `JSKELET_S3_API_URL` | `logs.s3` / asset upload | — | S3-compatible endpoint |
|
|
1218
1270
|
| `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) |
|
|
1219
1271
|
| `JSKELET_CLOUDFLARE_ZONE_ID` | Cloudflare cache surface | — | Zone identifier. No Cloudflare endpoint is called unless it is set alongside the token |
|
|
1220
1272
|
| `JSKELET_CLOUDFLARE_HOSTNAME` | Cloudflare cache surface | — | The root for purge URLs. Required when the panel is opened over an internal address |
|
package/docs/en/08-build.md
CHANGED
|
@@ -20,6 +20,7 @@ build is in [09-dev-tools.md](./09-dev-tools.md).
|
|
|
20
20
|
5. Images if config.images !== false, not watch, and sharp is installed
|
|
21
21
|
6. Manifest .jskelet/manifest.json
|
|
22
22
|
7. Precompress if not watch
|
|
23
|
+
8. Assets upload if not watch (logs `skipped (reason)` when off)
|
|
23
24
|
```
|
|
24
25
|
|
|
25
26
|
Template compilation finishes **before** asset scanning; there is no parse on
|
|
@@ -30,9 +31,11 @@ The order is not arbitrary:
|
|
|
30
31
|
|
|
31
32
|
- **CSS comes after the icon sprite.** The sprite is an asset and produces no
|
|
32
33
|
classes, but it does give a manifest key.
|
|
33
|
-
- **Precompress
|
|
34
|
-
|
|
34
|
+
- **Precompress runs just before upload:** the origin still gets `.br`/`.gz`;
|
|
35
|
+
the S3 step copies only the raw files.
|
|
35
36
|
- **Images never run on a watch pass:** re-encoding with `sharp` is expensive.
|
|
37
|
+
- **Assets upload never runs on watch:** only production `jskelet build`; when
|
|
38
|
+
off the step still prints (`skipped (no JSKELET_ASSETS_BUCKET)`, etc.).
|
|
36
39
|
|
|
37
40
|
Tasks only run if the relevant configuration exists. A project that does not
|
|
38
41
|
define any fonts never sees the font step; this is the build-side counterpart of
|
|
@@ -78,9 +81,26 @@ They are passed to templates automatically; in server code,
|
|
|
78
81
|
```
|
|
79
82
|
|
|
80
83
|
- `asset(name)` returns the hashed URL if it is in the manifest, otherwise
|
|
81
|
-
`/assets/<name>`.
|
|
84
|
+
`/assets/<name>`. When `build.assets.prefix` / `JSKELET_ASSET_PREFIX` is set,
|
|
85
|
+
that relative path is prefixed with the CDN origin (the on-disk manifest stays
|
|
86
|
+
relative).
|
|
82
87
|
- `hasAsset(name)` tells you whether it is in the manifest.
|
|
83
88
|
|
|
89
|
+
### CDN prefix and S3 upload
|
|
90
|
+
|
|
91
|
+
Off by default. When enabled:
|
|
92
|
+
|
|
93
|
+
1. A production build PutObjects files under `public/assets/` (and any other
|
|
94
|
+
directories in `include`) to S3.
|
|
95
|
+
2. At runtime `asset()` / optimised images emit absolute CDN URLs via
|
|
96
|
+
`JSKELET_ASSET_PREFIX`. When the entry script is on the CDN, esbuild chunks
|
|
97
|
+
resolve relatively on the same host.
|
|
98
|
+
|
|
99
|
+
Credentials: `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` first, else
|
|
100
|
+
`JSKELET_S3_*`. Bucket: `JSKELET_ASSETS_BUCKET`. Config reference:
|
|
101
|
+
[07-configuration.md](./07-configuration.md#build). Deploy notes:
|
|
102
|
+
[10-deployment.md](./10-deployment.md).
|
|
103
|
+
|
|
84
104
|
If the build has not run, the application still comes up: `hasAsset()` is false
|
|
85
105
|
and the layout never emits the stylesheet and script tags. When `jskelet build`
|
|
86
106
|
is forgotten you get an unstyled but working page instead of an error. If the
|
package/docs/en/10-deployment.md
CHANGED
|
@@ -52,7 +52,28 @@ considering in production:
|
|
|
52
52
|
| `PREWARM_MAX` | Depends on site size | Number of pages warmed at startup |
|
|
53
53
|
| `PREWARM_INTERVAL_SECONDS` | `0` or a long value | If you want to keep never-visited pages warm |
|
|
54
54
|
| `DEV_GATE` + `DEV_TOKEN` | Staging only | Hides an environment that is not public yet. The token alone does not lock the site |
|
|
55
|
-
| `JSKELET_S3_*` | If you write access logs to S3 |
|
|
55
|
+
| `JSKELET_S3_*` / `AWS_*` | If you write access logs or assets to S3 | Credentials; on AWS, `AWS_ACCESS_KEY_ID` etc. are enough. Details in [07](./07-configuration.md) |
|
|
56
|
+
| `JSKELET_ASSETS_BUCKET` | If you publish hashed assets to S3/CloudFront | PutObject at the end of production `jskelet build`; `JSKELET_ASSET_PREFIX` makes HTML emit CDN URLs |
|
|
57
|
+
|
|
58
|
+
### Publishing assets to S3 / CloudFront
|
|
59
|
+
|
|
60
|
+
Off by default. In CI or a production build environment:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
JSKELET_ASSETS_BUCKET=my-app-assets # or my-app-assets/static
|
|
64
|
+
JSKELET_ASSET_PREFIX=https://dxxxx.cloudfront.net
|
|
65
|
+
AWS_REGION=eu-central-1
|
|
66
|
+
AWS_ACCESS_KEY_ID=…
|
|
67
|
+
AWS_SECRET_ACCESS_KEY=…
|
|
68
|
+
# for temporary credentials:
|
|
69
|
+
# AWS_SESSION_TOKEN=…
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
When `jskelet build` finishes, files under `public/assets/` (and any other
|
|
73
|
+
`include` directories) are copied to S3; origin copies remain. If the bucket or
|
|
74
|
+
credentials are missing, the step logs `skipped (…)` with the reason and the
|
|
75
|
+
build continues. Watch / `jskelet dev` never upload. If you use CSP, add the CDN
|
|
76
|
+
origin to `script-src` / `style-src` / `img-src` and to `preconnect`.
|
|
56
77
|
|
|
57
78
|
When a file or S3 sink is enabled in production, the HTTP access log middleware
|
|
58
79
|
mounts automatically (if `http` is in `logs.kinds`). The admin panel ring is
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jskelet",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.9",
|
|
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
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tek build girişi: fontlar → ikon sprite → CSS → client JS → görseller →
|
|
3
|
-
* manifest → precompress.
|
|
3
|
+
* manifest → precompress → (opsiyonel) S3 asset upload.
|
|
4
4
|
*
|
|
5
5
|
* Sıra rastgele değil. CSS Tailwind için şablonları tarar, bu yüzden ikon
|
|
6
6
|
* sprite'ından sonra gelir (sprite bir varlık, sınıf üretmez ama manifest
|
|
7
|
-
* anahtarı verir). Precompress
|
|
8
|
-
*
|
|
7
|
+
* anahtarı verir). Precompress upload'tan önce: origin hâlâ `.br` üretir;
|
|
8
|
+
* S3 adımı ham dosyaları kopyalar. Upload yalnızca `build.assets.upload`
|
|
9
|
+
* açıksa ve watch değilse çalışır.
|
|
9
10
|
*
|
|
10
11
|
* Görevler yalnızca ilgili yapılandırma varsa çalışır. Font tanımlamayan bir
|
|
11
12
|
* proje font adımını hiç görmez; bu, "framework her projeye kendi
|
|
@@ -108,6 +109,14 @@ writeManifest(manifest);
|
|
|
108
109
|
if (!watch) {
|
|
109
110
|
const { precompressAssets } = await import("./tasks/precompress.mjs");
|
|
110
111
|
await task("Precompress", precompressAssets);
|
|
112
|
+
|
|
113
|
+
// Her production build'de satır görünür; kapalıysa `skipped (sebep)`.
|
|
114
|
+
// CDN'e basılacak dosyalar precompress'ten sonra hazır; `.br`/`.gz` yüklenmez.
|
|
115
|
+
{
|
|
116
|
+
const { uploadAssets } = await import("./tasks/assets-upload.mjs");
|
|
117
|
+
await task("Assets upload", () => uploadAssets(config));
|
|
118
|
+
}
|
|
119
|
+
|
|
111
120
|
log.section("output");
|
|
112
121
|
for (const row of manifestRows()) log.line(row);
|
|
113
122
|
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Production build sonunda hash'li varlıkları S3'e PutObject ile kopyalar.
|
|
3
|
+
*
|
|
4
|
+
* Watch turunda çalışmaz (çağıran sorumlu). Credential env'den gelir
|
|
5
|
+
* (`AWS_*` önce, `JSKELET_S3_*` yedek). Bucket / credential yoksa veya upload
|
|
6
|
+
* kapalıysa adım yine görünür: `skipped (…)` ile sebep basılır, build düşmez.
|
|
7
|
+
*
|
|
8
|
+
* `.br` / `.gz` kardeşleri yüklenmez: edge kendi sıkıştırmasını yapsın;
|
|
9
|
+
* origin precompress Express için kalır.
|
|
10
|
+
*/
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { readS3CredentialsFromEnv } from "../../config/index.js";
|
|
14
|
+
import { putObject } from "../../server/logs/s3-put.js";
|
|
15
|
+
import * as log from "../../log.mjs";
|
|
16
|
+
import { paths } from "../paths.mjs";
|
|
17
|
+
|
|
18
|
+
const CONCURRENCY = 8;
|
|
19
|
+
|
|
20
|
+
/** @type {Record<string, string>} */
|
|
21
|
+
const MIME = {
|
|
22
|
+
".js": "application/javascript; charset=utf-8",
|
|
23
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
24
|
+
".css": "text/css; charset=utf-8",
|
|
25
|
+
".svg": "image/svg+xml",
|
|
26
|
+
".png": "image/png",
|
|
27
|
+
".jpg": "image/jpeg",
|
|
28
|
+
".jpeg": "image/jpeg",
|
|
29
|
+
".webp": "image/webp",
|
|
30
|
+
".avif": "image/avif",
|
|
31
|
+
".gif": "image/gif",
|
|
32
|
+
".ico": "image/x-icon",
|
|
33
|
+
".woff": "font/woff",
|
|
34
|
+
".woff2": "font/woff2",
|
|
35
|
+
".ttf": "font/ttf",
|
|
36
|
+
".otf": "font/otf",
|
|
37
|
+
".json": "application/json",
|
|
38
|
+
".map": "application/json",
|
|
39
|
+
".txt": "text/plain; charset=utf-8",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} filePath
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
export function contentTypeFor(filePath) {
|
|
47
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
48
|
+
return MIME[ext] ?? "application/octet-stream";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {string} dir
|
|
53
|
+
* @returns {string[]} Mutlak dosya yolları
|
|
54
|
+
*/
|
|
55
|
+
export function listFilesRecursive(dir) {
|
|
56
|
+
if (!fs.existsSync(dir)) return [];
|
|
57
|
+
|
|
58
|
+
/** @type {string[]} */
|
|
59
|
+
const out = [];
|
|
60
|
+
const walk = (current) => {
|
|
61
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
62
|
+
const full = path.join(current, entry.name);
|
|
63
|
+
if (entry.isDirectory()) {
|
|
64
|
+
walk(full);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (!entry.isFile()) continue;
|
|
68
|
+
// Precompress kardeşleri origin'e özel; S3'e basma.
|
|
69
|
+
if (entry.name.endsWith(".br") || entry.name.endsWith(".gz")) continue;
|
|
70
|
+
out.push(full);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
walk(dir);
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* `include` adını `public/` altı mutlak dizine çevirir.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} name
|
|
81
|
+
* @returns {string | null}
|
|
82
|
+
*/
|
|
83
|
+
function resolveIncludeDir(name) {
|
|
84
|
+
if (name === "assets") return paths.assets;
|
|
85
|
+
if (name === "fonts") return paths.fonts;
|
|
86
|
+
if (!name || name.includes("..") || path.isAbsolute(name)) return null;
|
|
87
|
+
return path.join(paths.public, name);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Upload yapılmayacaksa kısa sebep; yapılacaksa `null`.
|
|
92
|
+
*
|
|
93
|
+
* @param {import('../../config/index.js').ResolvedConfig} config
|
|
94
|
+
* @returns {string | null}
|
|
95
|
+
*/
|
|
96
|
+
export function assetsUploadSkipReason(config) {
|
|
97
|
+
const upload = config.build?.assets?.upload;
|
|
98
|
+
|
|
99
|
+
if (!upload?.enabled) {
|
|
100
|
+
// Bucket yoksa asıl eksik env'i söyle — `enabled: false` çoğu zaman
|
|
101
|
+
// bucket tanımlanmadığı için dolaylıdır.
|
|
102
|
+
if (!upload?.bucket) return "no JSKELET_ASSETS_BUCKET";
|
|
103
|
+
return "build.assets.upload.enabled is false";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!upload.bucket) return "no JSKELET_ASSETS_BUCKET";
|
|
107
|
+
|
|
108
|
+
if (!readS3CredentialsFromEnv()) return "credentials missing";
|
|
109
|
+
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {import('../../config/index.js').ResolvedConfig} config
|
|
115
|
+
* @param {{ put?: typeof putObject }} [options]
|
|
116
|
+
* @returns {Promise<void>}
|
|
117
|
+
*/
|
|
118
|
+
export async function uploadAssets(config, options = {}) {
|
|
119
|
+
const reason = assetsUploadSkipReason(config);
|
|
120
|
+
if (reason) {
|
|
121
|
+
log.detail(`skipped (${reason})`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const upload = config.build.assets.upload;
|
|
126
|
+
const credentials = readS3CredentialsFromEnv();
|
|
127
|
+
// skipReason null iken credential ve bucket vardır; TS/akış için daralt.
|
|
128
|
+
if (!upload.bucket || !credentials) {
|
|
129
|
+
log.detail("skipped (credentials missing)");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const put = options.put ?? putObject;
|
|
134
|
+
const keyPrefix = (upload.keyPrefix || "").replace(/^\/+/, "");
|
|
135
|
+
|
|
136
|
+
/** @type {{ file: string, key: string }[]} */
|
|
137
|
+
const jobs = [];
|
|
138
|
+
for (const name of upload.include) {
|
|
139
|
+
const dir = resolveIncludeDir(name);
|
|
140
|
+
if (!dir) {
|
|
141
|
+
console.warn(`[assets] upload.include "${name}" is invalid, skipping`);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
for (const file of listFilesRecursive(dir)) {
|
|
145
|
+
const relative = path
|
|
146
|
+
.relative(paths.public, file)
|
|
147
|
+
.split(path.sep)
|
|
148
|
+
.join("/");
|
|
149
|
+
if (relative.startsWith("..")) continue;
|
|
150
|
+
jobs.push({ file, key: `${keyPrefix}${relative}` });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!jobs.length) {
|
|
155
|
+
log.detail("0 files");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let uploaded = 0;
|
|
160
|
+
let index = 0;
|
|
161
|
+
|
|
162
|
+
/** @returns {Promise<void>} */
|
|
163
|
+
const worker = async () => {
|
|
164
|
+
while (index < jobs.length) {
|
|
165
|
+
const job = jobs[index++];
|
|
166
|
+
const body = fs.readFileSync(job.file);
|
|
167
|
+
await put({
|
|
168
|
+
bucket: upload.bucket,
|
|
169
|
+
key: job.key,
|
|
170
|
+
body,
|
|
171
|
+
region: upload.region,
|
|
172
|
+
endpoint: upload.endpoint,
|
|
173
|
+
credentials,
|
|
174
|
+
contentType: contentTypeFor(job.file),
|
|
175
|
+
cacheControl: upload.cacheControl,
|
|
176
|
+
});
|
|
177
|
+
uploaded += 1;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const workers = Array.from(
|
|
182
|
+
{ length: Math.min(CONCURRENCY, jobs.length) },
|
|
183
|
+
() => worker(),
|
|
184
|
+
);
|
|
185
|
+
await Promise.all(workers);
|
|
186
|
+
|
|
187
|
+
const target = `${upload.bucket}/${keyPrefix}`.replace(/\/+/g, "/");
|
|
188
|
+
const via = upload.endpoint ? ` @ ${upload.endpoint}` : "";
|
|
189
|
+
log.detail(`${uploaded} → s3://${target}${via}`);
|
|
190
|
+
}
|
package/src/config/defaults.js
CHANGED
|
@@ -471,10 +471,14 @@ export const DEFAULT_SECURITY = {
|
|
|
471
471
|
};
|
|
472
472
|
|
|
473
473
|
/**
|
|
474
|
-
* Client bundle (esbuild)
|
|
474
|
+
* Client bundle (esbuild) ayarları + opsiyonel CDN prefix / S3 upload.
|
|
475
475
|
*
|
|
476
476
|
* `groups` / `chunkGroup` esbuild'in native manualChunks'ı olmadığı için
|
|
477
477
|
* framework plugin'i ile uygulanır — ayrıntı `src/build/chunk-groups.mjs`.
|
|
478
|
+
*
|
|
479
|
+
* `assets.prefix` HTML'deki `asset()` URL'lerine eklenir (manifest diskte
|
|
480
|
+
* göreli kalır). `assets.upload` production build sonunda `public/` altındaki
|
|
481
|
+
* seçili dizinleri S3'e PutObject ile kopyalar; credential env'den okunur.
|
|
478
482
|
*/
|
|
479
483
|
export const DEFAULT_BUILD = {
|
|
480
484
|
client: {
|
|
@@ -493,6 +497,21 @@ export const DEFAULT_BUILD = {
|
|
|
493
497
|
*/
|
|
494
498
|
chunkGroup: null,
|
|
495
499
|
},
|
|
500
|
+
assets: {
|
|
501
|
+
/** CDN / CloudFront origin; sonunda `/` yok. Env: `JSKELET_ASSET_PREFIX`. */
|
|
502
|
+
prefix: "",
|
|
503
|
+
upload: {
|
|
504
|
+
enabled: false,
|
|
505
|
+
bucket: null,
|
|
506
|
+
/** Nesne anahtarı öneki (`bucket/path` split'inden veya config). */
|
|
507
|
+
keyPrefix: "",
|
|
508
|
+
region: "us-east-1",
|
|
509
|
+
endpoint: null,
|
|
510
|
+
/** `public/` altı dizin adları. */
|
|
511
|
+
include: /** @type {string[]} */ (["assets"]),
|
|
512
|
+
cacheControl: "public, max-age=31536000, immutable",
|
|
513
|
+
},
|
|
514
|
+
},
|
|
496
515
|
};
|
|
497
516
|
|
|
498
517
|
/**
|
package/src/config/index.js
CHANGED
|
@@ -161,7 +161,7 @@ const CONFIG_FILE = "jskelet.config.mjs";
|
|
|
161
161
|
* @property {{ scan?: string[], dir: string } | false} icons
|
|
162
162
|
* @property {ImagesConfig | false} images
|
|
163
163
|
* @property {string[]} clientEnv Client bundle'a gömülecek env anahtarları.
|
|
164
|
-
* @property {BuildConfig} build Client bundling
|
|
164
|
+
* @property {BuildConfig} build Client bundling + CDN prefix / S3 asset upload.
|
|
165
165
|
*/
|
|
166
166
|
|
|
167
167
|
/**
|
|
@@ -170,8 +170,22 @@ const CONFIG_FILE = "jskelet.config.mjs";
|
|
|
170
170
|
* @property {Record<string, string[]>} groups
|
|
171
171
|
* @property {((id: string) => string | null | undefined) | null} chunkGroup
|
|
172
172
|
*
|
|
173
|
+
* @typedef {object} BuildAssetsUploadConfig
|
|
174
|
+
* @property {boolean} enabled
|
|
175
|
+
* @property {string | null} bucket
|
|
176
|
+
* @property {string} keyPrefix
|
|
177
|
+
* @property {string} region
|
|
178
|
+
* @property {string | null} endpoint
|
|
179
|
+
* @property {string[]} include
|
|
180
|
+
* @property {string} cacheControl
|
|
181
|
+
*
|
|
182
|
+
* @typedef {object} BuildAssetsConfig
|
|
183
|
+
* @property {string} prefix CDN origin (`JSKELET_ASSET_PREFIX`); sonunda `/` yok.
|
|
184
|
+
* @property {BuildAssetsUploadConfig} upload
|
|
185
|
+
*
|
|
173
186
|
* @typedef {object} BuildConfig
|
|
174
187
|
* @property {BuildClientConfig} client
|
|
188
|
+
* @property {BuildAssetsConfig} assets
|
|
175
189
|
*/
|
|
176
190
|
|
|
177
191
|
/**
|
|
@@ -514,19 +528,28 @@ function envText(value) {
|
|
|
514
528
|
}
|
|
515
529
|
|
|
516
530
|
/**
|
|
531
|
+
* S3 imza credential'ları. AWS deploy'da standart env önce gelir; R2/MinIO
|
|
532
|
+
* için `JSKELET_S3_*` yedek. IAM instance/task role (IMDS) desteklenmez —
|
|
533
|
+
* CI'da `configure-aws-credentials` gibi araçlar zaten `AWS_*` basar.
|
|
534
|
+
*
|
|
517
535
|
* @returns {{ accessKeyId: string, secretAccessKey: string,
|
|
518
536
|
* sessionToken: string | null } | null}
|
|
519
537
|
*/
|
|
520
538
|
export function readS3CredentialsFromEnv() {
|
|
521
|
-
const accessKeyId =
|
|
539
|
+
const accessKeyId =
|
|
540
|
+
envText(process.env.AWS_ACCESS_KEY_ID) ??
|
|
541
|
+
envText(process.env.JSKELET_S3_ACCESS_KEY_ID);
|
|
522
542
|
const secretAccessKey =
|
|
543
|
+
envText(process.env.AWS_SECRET_ACCESS_KEY) ??
|
|
523
544
|
envText(process.env.JSKELET_S3_SECRET_ACCESS_KEY) ??
|
|
524
545
|
envText(process.env.JSKELET_S3_ACCESS_SECRET);
|
|
525
546
|
if (!accessKeyId || !secretAccessKey) return null;
|
|
526
547
|
return {
|
|
527
548
|
accessKeyId,
|
|
528
549
|
secretAccessKey,
|
|
529
|
-
sessionToken:
|
|
550
|
+
sessionToken:
|
|
551
|
+
envText(process.env.AWS_SESSION_TOKEN) ??
|
|
552
|
+
envText(process.env.JSKELET_S3_SESSION_TOKEN),
|
|
530
553
|
};
|
|
531
554
|
}
|
|
532
555
|
|
|
@@ -1152,7 +1175,122 @@ function normalizeSecurity(raw) {
|
|
|
1152
1175
|
}
|
|
1153
1176
|
|
|
1154
1177
|
/**
|
|
1155
|
-
*
|
|
1178
|
+
* @returns {BuildClientConfig}
|
|
1179
|
+
*/
|
|
1180
|
+
function defaultBuildClient() {
|
|
1181
|
+
return {
|
|
1182
|
+
splitting: DEFAULT_BUILD.client.splitting,
|
|
1183
|
+
groups: {},
|
|
1184
|
+
chunkGroup: null,
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/**
|
|
1189
|
+
* CDN prefix + S3 upload (`build.assets`). Credential config'e yazılmaz.
|
|
1190
|
+
*
|
|
1191
|
+
* @param {unknown} raw
|
|
1192
|
+
* @returns {BuildAssetsConfig}
|
|
1193
|
+
*/
|
|
1194
|
+
export function normalizeBuildAssets(raw) {
|
|
1195
|
+
const source =
|
|
1196
|
+
raw != null && typeof raw === "object" && !Array.isArray(raw)
|
|
1197
|
+
? /** @type {Record<string, any>} */ (raw)
|
|
1198
|
+
: {};
|
|
1199
|
+
|
|
1200
|
+
if (raw != null && (typeof raw !== "object" || Array.isArray(raw))) {
|
|
1201
|
+
console.warn("[config] build.assets must be an object, ignoring it");
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
const envPrefix = envText(process.env.JSKELET_ASSET_PREFIX);
|
|
1205
|
+
const configPrefix =
|
|
1206
|
+
typeof source.prefix === "string" ? source.prefix.trim() : "";
|
|
1207
|
+
const prefix = (envPrefix ?? configPrefix).replace(/\/+$/, "");
|
|
1208
|
+
|
|
1209
|
+
const uploadRaw =
|
|
1210
|
+
source.upload != null &&
|
|
1211
|
+
typeof source.upload === "object" &&
|
|
1212
|
+
!Array.isArray(source.upload)
|
|
1213
|
+
? /** @type {Record<string, any>} */ (source.upload)
|
|
1214
|
+
: {};
|
|
1215
|
+
|
|
1216
|
+
if (
|
|
1217
|
+
source.upload != null &&
|
|
1218
|
+
(typeof source.upload !== "object" || Array.isArray(source.upload))
|
|
1219
|
+
) {
|
|
1220
|
+
console.warn("[config] build.assets.upload must be an object, ignoring it");
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
const envBucket = envText(process.env.JSKELET_ASSETS_BUCKET);
|
|
1224
|
+
const bucketPath = splitS3BucketPath(
|
|
1225
|
+
envBucket ?? envText(uploadRaw.bucket),
|
|
1226
|
+
);
|
|
1227
|
+
|
|
1228
|
+
const configKeyPrefix =
|
|
1229
|
+
typeof uploadRaw.keyPrefix === "string" && uploadRaw.keyPrefix
|
|
1230
|
+
? uploadRaw.keyPrefix.endsWith("/")
|
|
1231
|
+
? uploadRaw.keyPrefix
|
|
1232
|
+
: `${uploadRaw.keyPrefix}/`
|
|
1233
|
+
: typeof uploadRaw.prefix === "string" && uploadRaw.prefix
|
|
1234
|
+
? uploadRaw.prefix.endsWith("/")
|
|
1235
|
+
? uploadRaw.prefix
|
|
1236
|
+
: `${uploadRaw.prefix}/`
|
|
1237
|
+
: DEFAULT_BUILD.assets.upload.keyPrefix;
|
|
1238
|
+
|
|
1239
|
+
const endpoint =
|
|
1240
|
+
envText(process.env.AWS_ENDPOINT_URL) ??
|
|
1241
|
+
envText(process.env.JSKELET_S3_API_URL) ??
|
|
1242
|
+
envText(uploadRaw.endpoint);
|
|
1243
|
+
|
|
1244
|
+
// Gerçek AWS'te region imza için gerekir; custom endpoint'te `auto` yaygın.
|
|
1245
|
+
const region =
|
|
1246
|
+
envText(uploadRaw.region) ??
|
|
1247
|
+
envText(process.env.AWS_REGION) ??
|
|
1248
|
+
envText(process.env.AWS_DEFAULT_REGION) ??
|
|
1249
|
+
envText(process.env.JSKELET_S3_REGION) ??
|
|
1250
|
+
(endpoint ? "auto" : DEFAULT_BUILD.assets.upload.region);
|
|
1251
|
+
|
|
1252
|
+
/** @type {string[]} */
|
|
1253
|
+
let include = [...DEFAULT_BUILD.assets.upload.include];
|
|
1254
|
+
if (uploadRaw.include != null) {
|
|
1255
|
+
const list = asArray(uploadRaw.include, "build.assets.upload.include")
|
|
1256
|
+
.filter((entry) => typeof entry === "string" && entry.trim())
|
|
1257
|
+
.map((entry) => String(entry).trim().replace(/^\/+|\/+$/g, ""));
|
|
1258
|
+
if (list.length) include = [...new Set(list)];
|
|
1259
|
+
else {
|
|
1260
|
+
console.warn(
|
|
1261
|
+
"[config] build.assets.upload.include has no valid entries, using defaults",
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
const cacheControl =
|
|
1267
|
+
typeof uploadRaw.cacheControl === "string" && uploadRaw.cacheControl.trim()
|
|
1268
|
+
? uploadRaw.cacheControl.trim()
|
|
1269
|
+
: DEFAULT_BUILD.assets.upload.cacheControl;
|
|
1270
|
+
|
|
1271
|
+
// `JSKELET_ASSETS_BUCKET` doluysa config'te `enabled` unutulmuş olsa bile aç.
|
|
1272
|
+
// Açık `enabled: false` ezer.
|
|
1273
|
+
const enabled =
|
|
1274
|
+
uploadRaw.enabled === true ||
|
|
1275
|
+
(uploadRaw.enabled !== false && Boolean(bucketPath.bucket));
|
|
1276
|
+
|
|
1277
|
+
return {
|
|
1278
|
+
prefix,
|
|
1279
|
+
upload: {
|
|
1280
|
+
enabled,
|
|
1281
|
+
bucket: bucketPath.bucket,
|
|
1282
|
+
keyPrefix: bucketPath.prefix ?? configKeyPrefix,
|
|
1283
|
+
region,
|
|
1284
|
+
endpoint,
|
|
1285
|
+
include,
|
|
1286
|
+
cacheControl,
|
|
1287
|
+
},
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* Client build ayarları (`build.client` + `build.assets`). Bozuk tipler
|
|
1293
|
+
* uyarıyla varsayılana düşer.
|
|
1156
1294
|
*
|
|
1157
1295
|
* @param {unknown} raw
|
|
1158
1296
|
* @returns {BuildConfig}
|
|
@@ -1160,47 +1298,30 @@ function normalizeSecurity(raw) {
|
|
|
1160
1298
|
export function normalizeBuild(raw) {
|
|
1161
1299
|
if (raw == null) {
|
|
1162
1300
|
return {
|
|
1163
|
-
client:
|
|
1164
|
-
|
|
1165
|
-
groups: {},
|
|
1166
|
-
chunkGroup: null,
|
|
1167
|
-
},
|
|
1301
|
+
client: defaultBuildClient(),
|
|
1302
|
+
assets: normalizeBuildAssets(null),
|
|
1168
1303
|
};
|
|
1169
1304
|
}
|
|
1170
1305
|
|
|
1171
1306
|
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
1172
1307
|
console.warn("[config] build must be an object, ignoring it");
|
|
1173
1308
|
return {
|
|
1174
|
-
client:
|
|
1175
|
-
|
|
1176
|
-
groups: {},
|
|
1177
|
-
chunkGroup: null,
|
|
1178
|
-
},
|
|
1309
|
+
client: defaultBuildClient(),
|
|
1310
|
+
assets: normalizeBuildAssets(null),
|
|
1179
1311
|
};
|
|
1180
1312
|
}
|
|
1181
1313
|
|
|
1182
1314
|
const source = /** @type {Record<string, any>} */ (raw);
|
|
1315
|
+
const assets = normalizeBuildAssets(source.assets);
|
|
1183
1316
|
const clientRaw = source.client;
|
|
1184
1317
|
|
|
1185
1318
|
if (clientRaw == null) {
|
|
1186
|
-
return {
|
|
1187
|
-
client: {
|
|
1188
|
-
splitting: DEFAULT_BUILD.client.splitting,
|
|
1189
|
-
groups: {},
|
|
1190
|
-
chunkGroup: null,
|
|
1191
|
-
},
|
|
1192
|
-
};
|
|
1319
|
+
return { client: defaultBuildClient(), assets };
|
|
1193
1320
|
}
|
|
1194
1321
|
|
|
1195
1322
|
if (typeof clientRaw !== "object" || Array.isArray(clientRaw)) {
|
|
1196
1323
|
console.warn("[config] build.client must be an object, ignoring it");
|
|
1197
|
-
return {
|
|
1198
|
-
client: {
|
|
1199
|
-
splitting: DEFAULT_BUILD.client.splitting,
|
|
1200
|
-
groups: {},
|
|
1201
|
-
chunkGroup: null,
|
|
1202
|
-
},
|
|
1203
|
-
};
|
|
1324
|
+
return { client: defaultBuildClient(), assets };
|
|
1204
1325
|
}
|
|
1205
1326
|
|
|
1206
1327
|
const client = /** @type {Record<string, any>} */ (clientRaw);
|
|
@@ -1250,7 +1371,7 @@ export function normalizeBuild(raw) {
|
|
|
1250
1371
|
);
|
|
1251
1372
|
}
|
|
1252
1373
|
|
|
1253
|
-
return { client: { splitting, groups, chunkGroup } };
|
|
1374
|
+
return { client: { splitting, groups, chunkGroup }, assets };
|
|
1254
1375
|
}
|
|
1255
1376
|
|
|
1256
1377
|
/**
|
package/src/index.js
CHANGED
|
@@ -43,7 +43,13 @@ export {
|
|
|
43
43
|
writeSharedCookie,
|
|
44
44
|
} from "./http/shared-cookie.js";
|
|
45
45
|
export { reportUpstreamFailure } from "./server/upstream-tracking.js";
|
|
46
|
-
export {
|
|
46
|
+
export {
|
|
47
|
+
asset,
|
|
48
|
+
hasAsset,
|
|
49
|
+
optimizedImage,
|
|
50
|
+
getSpriteIds,
|
|
51
|
+
applyAssetPrefix,
|
|
52
|
+
} from "./server/assets.js";
|
|
47
53
|
export { remoteImageUrl, parseAllowedRemoteUrl } from "./server/image-optimizer.js";
|
|
48
54
|
export {
|
|
49
55
|
ImageResponse,
|
package/src/server/assets.js
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* döner ve `hasAsset()` false olur; layout stylesheet/script etiketlerini hiç
|
|
6
6
|
* basmaz. Böylece `jskelet build` unutulduğunda hata yerine stilsiz ama
|
|
7
7
|
* çalışan bir sayfa görürsünüz.
|
|
8
|
+
*
|
|
9
|
+
* `build.assets.prefix` / `JSKELET_ASSET_PREFIX` doluysa HTML'e basılan
|
|
10
|
+
* URL'lere CDN origin eklenir; manifest diskte göreli kalır (çok ortamlı
|
|
11
|
+
* tek artifact). Dosya sistemi okumaları (`getSpriteIds`) göreli yolu kullanır.
|
|
8
12
|
*/
|
|
9
13
|
import fs from "node:fs";
|
|
10
14
|
import path from "node:path";
|
|
@@ -44,12 +48,45 @@ function state() {
|
|
|
44
48
|
return cached;
|
|
45
49
|
}
|
|
46
50
|
|
|
51
|
+
/**
|
|
52
|
+
* CDN prefix + göreli asset yolu. Zaten mutlak (`https:…`) URL'lere dokunmaz.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} prefix Sonunda `/` olmadan (normalize edilmiş).
|
|
55
|
+
* @param {string} url
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
export function applyAssetPrefix(prefix, url) {
|
|
59
|
+
if (!prefix || !url) return url;
|
|
60
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(url)) return url;
|
|
61
|
+
const pathPart = url.startsWith("/") ? url : `/${url}`;
|
|
62
|
+
return `${prefix.replace(/\/+$/, "")}${pathPart}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** @returns {string} */
|
|
66
|
+
function assetPrefix() {
|
|
67
|
+
try {
|
|
68
|
+
return getConfig().build?.assets?.prefix ?? "";
|
|
69
|
+
} catch {
|
|
70
|
+
return "";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Manifest'teki göreli yol (prefix yok) — disk okuma için.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} name
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
function assetPath(name) {
|
|
81
|
+
return state().manifest[name] ?? `/assets/${name}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
47
84
|
/**
|
|
48
85
|
* @param {string} name Örn. "app.css", "main.js", "sprite.svg"
|
|
49
86
|
* @returns {string}
|
|
50
87
|
*/
|
|
51
88
|
export function asset(name) {
|
|
52
|
-
return
|
|
89
|
+
return applyAssetPrefix(assetPrefix(), assetPath(name));
|
|
53
90
|
}
|
|
54
91
|
|
|
55
92
|
/**
|
|
@@ -113,7 +150,20 @@ export function optimizedImage(src) {
|
|
|
113
150
|
}
|
|
114
151
|
}
|
|
115
152
|
|
|
116
|
-
|
|
153
|
+
if (!src) return undefined;
|
|
154
|
+
const entry = images[src];
|
|
155
|
+
if (!entry) return undefined;
|
|
156
|
+
|
|
157
|
+
const prefix = assetPrefix();
|
|
158
|
+
if (!prefix) return entry;
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
...entry,
|
|
162
|
+
variants: entry.variants.map((variant) => ({
|
|
163
|
+
...variant,
|
|
164
|
+
url: applyAssetPrefix(prefix, variant.url),
|
|
165
|
+
})),
|
|
166
|
+
};
|
|
117
167
|
}
|
|
118
168
|
|
|
119
169
|
/** @type {{ url: string, ids: Set<string> } | null} */
|
|
@@ -128,20 +178,21 @@ let spriteIds = null;
|
|
|
128
178
|
* @returns {Set<string>}
|
|
129
179
|
*/
|
|
130
180
|
export function getSpriteIds() {
|
|
131
|
-
|
|
132
|
-
|
|
181
|
+
// Disk yolu göreli olmalı; CDN prefix dosya sisteminde yok.
|
|
182
|
+
const relative = assetPath("sprite.svg");
|
|
183
|
+
if (spriteIds?.url === relative) return spriteIds.ids;
|
|
133
184
|
|
|
134
185
|
/** @type {Set<string>} */
|
|
135
186
|
const ids = new Set();
|
|
136
187
|
|
|
137
188
|
try {
|
|
138
|
-
const file = path.join(getConfig().dirs.public,
|
|
189
|
+
const file = path.join(getConfig().dirs.public, relative.replace(/^\//, ""));
|
|
139
190
|
const svg = fs.readFileSync(file, "utf8");
|
|
140
191
|
for (const match of svg.matchAll(/<symbol id="([^"]+)"/g)) ids.add(match[1]);
|
|
141
192
|
} catch {
|
|
142
193
|
// Sprite okunamazsa uyarı üretilmez; asıl akış etkilenmez.
|
|
143
194
|
}
|
|
144
195
|
|
|
145
|
-
spriteIds = { url, ids };
|
|
196
|
+
spriteIds = { url: relative, ids };
|
|
146
197
|
return ids;
|
|
147
198
|
}
|
|
@@ -80,7 +80,7 @@ export async function configureLogs(config) {
|
|
|
80
80
|
);
|
|
81
81
|
} else if (!credentials) {
|
|
82
82
|
console.warn(
|
|
83
|
-
"[logs] s3.enabled but credentials missing — set
|
|
83
|
+
"[logs] s3.enabled but credentials missing — set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY or JSKELET_S3_*",
|
|
84
84
|
);
|
|
85
85
|
} else {
|
|
86
86
|
const region = logs.s3.region || "auto";
|
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
* Minimal S3 PutObject — yalnızca SigV4 + `fetch`.
|
|
3
3
|
*
|
|
4
4
|
* `@aws-sdk/client-s3` taşınmıyor: tek operasyon için SDK'nın maliyeti ve
|
|
5
|
-
* peer yüzeyi gereksiz. Credential zinciri env'den okunur
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
8
|
-
* container'a key vermeyen kurulumlar için sink zaten açılışta no-op'a düşer.
|
|
5
|
+
* peer yüzeyi gereksiz. Credential zinciri env'den okunur (`AWS_*` önce,
|
|
6
|
+
* sonra `JSKELET_S3_*`); IAM role / instance metadata yok — CI'da
|
|
7
|
+
* `configure-aws-credentials` gibi araçlar key basar.
|
|
9
8
|
*/
|
|
10
9
|
import crypto from "node:crypto";
|
|
11
10
|
|
|
@@ -15,7 +14,7 @@ import crypto from "node:crypto";
|
|
|
15
14
|
*
|
|
16
15
|
* @typedef {{ bucket: string, key: string, body: string | Buffer,
|
|
17
16
|
* region: string, endpoint?: string | null, credentials: AwsCredentials,
|
|
18
|
-
* now?: Date }} PutObjectInput
|
|
17
|
+
* contentType?: string, cacheControl?: string | null, now?: Date }} PutObjectInput
|
|
19
18
|
*/
|
|
20
19
|
|
|
21
20
|
/**
|
|
@@ -145,12 +144,17 @@ export function buildSignedPutObject(input) {
|
|
|
145
144
|
/** @type {Record<string, string>} */
|
|
146
145
|
const headers = {
|
|
147
146
|
host,
|
|
148
|
-
|
|
147
|
+
// Log sink varsayılanı; asset upload `contentType` geçirir.
|
|
148
|
+
"content-type": input.contentType ?? "application/x-ndjson",
|
|
149
149
|
"content-length": String(body.length),
|
|
150
150
|
"x-amz-content-sha256": payloadHash,
|
|
151
151
|
"x-amz-date": amzDate,
|
|
152
152
|
};
|
|
153
153
|
|
|
154
|
+
if (input.cacheControl) {
|
|
155
|
+
headers["cache-control"] = input.cacheControl;
|
|
156
|
+
}
|
|
157
|
+
|
|
154
158
|
if (input.credentials.sessionToken) {
|
|
155
159
|
headers["x-amz-security-token"] = input.credentials.sessionToken;
|
|
156
160
|
}
|
|
@@ -421,10 +421,14 @@ export declare const DEFAULT_SECURITY: {
|
|
|
421
421
|
};
|
|
422
422
|
};
|
|
423
423
|
/**
|
|
424
|
-
* Client bundle (esbuild)
|
|
424
|
+
* Client bundle (esbuild) ayarları + opsiyonel CDN prefix / S3 upload.
|
|
425
425
|
*
|
|
426
426
|
* `groups` / `chunkGroup` esbuild'in native manualChunks'ı olmadığı için
|
|
427
427
|
* framework plugin'i ile uygulanır — ayrıntı `src/build/chunk-groups.mjs`.
|
|
428
|
+
*
|
|
429
|
+
* `assets.prefix` HTML'deki `asset()` URL'lerine eklenir (manifest diskte
|
|
430
|
+
* göreli kalır). `assets.upload` production build sonunda `public/` altındaki
|
|
431
|
+
* seçili dizinleri S3'e PutObject ile kopyalar; credential env'den okunur.
|
|
428
432
|
*/
|
|
429
433
|
export declare const DEFAULT_BUILD: {
|
|
430
434
|
client: {
|
|
@@ -443,6 +447,21 @@ export declare const DEFAULT_BUILD: {
|
|
|
443
447
|
*/
|
|
444
448
|
chunkGroup: ((id: string) => string | null | undefined) | null;
|
|
445
449
|
};
|
|
450
|
+
assets: {
|
|
451
|
+
/** CDN / CloudFront origin; sonunda `/` yok. Env: `JSKELET_ASSET_PREFIX`. */
|
|
452
|
+
prefix: string;
|
|
453
|
+
upload: {
|
|
454
|
+
enabled: boolean;
|
|
455
|
+
bucket: null;
|
|
456
|
+
/** Nesne anahtarı öneki (`bucket/path` split'inden veya config). */
|
|
457
|
+
keyPrefix: string;
|
|
458
|
+
region: string;
|
|
459
|
+
endpoint: null;
|
|
460
|
+
/** `public/` altı dizin adları. */
|
|
461
|
+
include: string[];
|
|
462
|
+
cacheControl: string;
|
|
463
|
+
};
|
|
464
|
+
};
|
|
446
465
|
};
|
|
447
466
|
/**
|
|
448
467
|
* Build-zamanı görsel adımı + opsiyonel runtime uzak görsel proxy.
|
package/types/config/index.d.ts
CHANGED
|
@@ -221,7 +221,7 @@ export type ResolvedConfig = {
|
|
|
221
221
|
*/
|
|
222
222
|
clientEnv: string[];
|
|
223
223
|
/**
|
|
224
|
-
* Client bundling
|
|
224
|
+
* Client bundling + CDN prefix / S3 asset upload.
|
|
225
225
|
*/
|
|
226
226
|
build: BuildConfig;
|
|
227
227
|
};
|
|
@@ -230,8 +230,25 @@ export type BuildClientConfig = {
|
|
|
230
230
|
groups: Record<string, string[]>;
|
|
231
231
|
chunkGroup: ((id: string) => string | null | undefined) | null;
|
|
232
232
|
};
|
|
233
|
+
export type BuildAssetsUploadConfig = {
|
|
234
|
+
enabled: boolean;
|
|
235
|
+
bucket: string | null;
|
|
236
|
+
keyPrefix: string;
|
|
237
|
+
region: string;
|
|
238
|
+
endpoint: string | null;
|
|
239
|
+
include: string[];
|
|
240
|
+
cacheControl: string;
|
|
241
|
+
};
|
|
242
|
+
export type BuildAssetsConfig = {
|
|
243
|
+
/**
|
|
244
|
+
* CDN origin (`JSKELET_ASSET_PREFIX`); sonunda `/` yok.
|
|
245
|
+
*/
|
|
246
|
+
prefix: string;
|
|
247
|
+
upload: BuildAssetsUploadConfig;
|
|
248
|
+
};
|
|
233
249
|
export type BuildConfig = {
|
|
234
250
|
client: BuildClientConfig;
|
|
251
|
+
assets: BuildAssetsConfig;
|
|
235
252
|
};
|
|
236
253
|
export type ImagesRemoteConfig = {
|
|
237
254
|
enabled: boolean;
|
|
@@ -262,6 +279,10 @@ export declare function splitS3BucketPath(value: string | null): {
|
|
|
262
279
|
prefix: string | null;
|
|
263
280
|
};
|
|
264
281
|
/**
|
|
282
|
+
* S3 imza credential'ları. AWS deploy'da standart env önce gelir; R2/MinIO
|
|
283
|
+
* için `JSKELET_S3_*` yedek. IAM instance/task role (IMDS) desteklenmez —
|
|
284
|
+
* CI'da `configure-aws-credentials` gibi araçlar zaten `AWS_*` basar.
|
|
285
|
+
*
|
|
265
286
|
* @returns {{ accessKeyId: string, secretAccessKey: string,
|
|
266
287
|
* sessionToken: string | null } | null}
|
|
267
288
|
*/
|
|
@@ -292,7 +313,15 @@ export type SecurityConfig = {
|
|
|
292
313
|
};
|
|
293
314
|
};
|
|
294
315
|
/**
|
|
295
|
-
*
|
|
316
|
+
* CDN prefix + S3 upload (`build.assets`). Credential config'e yazılmaz.
|
|
317
|
+
*
|
|
318
|
+
* @param {unknown} raw
|
|
319
|
+
* @returns {BuildAssetsConfig}
|
|
320
|
+
*/
|
|
321
|
+
export declare function normalizeBuildAssets(raw: unknown): BuildAssetsConfig;
|
|
322
|
+
/**
|
|
323
|
+
* Client build ayarları (`build.client` + `build.assets`). Bozuk tipler
|
|
324
|
+
* uyarıyla varsayılana düşer.
|
|
296
325
|
*
|
|
297
326
|
* @param {unknown} raw
|
|
298
327
|
* @returns {BuildConfig}
|
package/types/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export { cache, withRequestCache } from "./http/request-cache.js";
|
|
|
13
13
|
export { clearCookie, getSignedCookie, parseCookies, randomToken, safeEqual, serializeCookie, setCookie, setSignedCookie, } from "./http/cookies.js";
|
|
14
14
|
export { clearSharedCookie, requestIsHttps, resolveSharedCookieDomain, SHARED_COOKIE_WARN_BYTES, writeSharedCookie, } from "./http/shared-cookie.js";
|
|
15
15
|
export { reportUpstreamFailure } from "./server/upstream-tracking.js";
|
|
16
|
-
export { asset, hasAsset, optimizedImage, getSpriteIds } from "./server/assets.js";
|
|
16
|
+
export { asset, hasAsset, optimizedImage, getSpriteIds, applyAssetPrefix, } from "./server/assets.js";
|
|
17
17
|
export { remoteImageUrl, parseAllowedRemoteUrl } from "./server/image-optimizer.js";
|
|
18
18
|
export { ImageResponse, OG_SIZE, buildOgSvg, escapeXml, ogHandler, ogImage, sendOgImage, wrapText, } from "./server/og-image.js";
|
|
19
19
|
export { headHints } from "./server/head-hints.js";
|
package/types/server/assets.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDN prefix + göreli asset yolu. Zaten mutlak (`https:…`) URL'lere dokunmaz.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} prefix Sonunda `/` olmadan (normalize edilmiş).
|
|
5
|
+
* @param {string} url
|
|
6
|
+
* @returns {string}
|
|
7
|
+
*/
|
|
8
|
+
export declare function applyAssetPrefix(prefix: string, url: string): string;
|
|
1
9
|
/**
|
|
2
10
|
* @param {string} name Örn. "app.css", "main.js", "sprite.svg"
|
|
3
11
|
* @returns {string}
|