jskelet 0.6.15 → 0.6.16
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 +6 -0
- package/docs/07-yapilandirma.md +2 -1
- package/docs/08-build.md +19 -6
- package/docs/en/07-configuration.md +2 -1
- 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 +12 -2
- package/src/config/index.js +16 -0
- package/src/server/logs/s3-put.js +148 -16
- package/types/config/defaults.d.ts +11 -1
- package/types/config/index.d.ts +4 -0
- package/types/server/logs/s3-put.d.ts +34 -0
package/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,12 @@ one is listed under a **Breaking** heading.
|
|
|
17
17
|
|
|
18
18
|
### Added
|
|
19
19
|
|
|
20
|
+
- When `build.assets.prefix` is set, the CSS build rewrites root-absolute
|
|
21
|
+
`url()` paths under `static.prefixes` (e.g. `/fonts/…`) to the CDN
|
|
22
|
+
prefix so they match the S3 key (`…/js/fonts/…`). Default upload
|
|
23
|
+
`include` is `["assets", "fonts"]`; prefix also forces `fonts` into
|
|
24
|
+
`include`. Font uploads run PutBucketCors with
|
|
25
|
+
`upload.corsOrigin` (default `"*"`, set `null` to skip).
|
|
20
26
|
- `asset(name, { origin: "document" })` skips `build.assets.prefix` and
|
|
21
27
|
returns the site-origin `/assets/…` path. `icon()` always resolves the
|
|
22
28
|
sprite this way so SVG `<use href>` keeps working when other assets are
|
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` /
|
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
|
|
@@ -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` /
|
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.16",
|
|
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
|
@@ -508,9 +508,19 @@ export const DEFAULT_BUILD = {
|
|
|
508
508
|
keyPrefix: "",
|
|
509
509
|
region: "us-east-1",
|
|
510
510
|
endpoint: null,
|
|
511
|
-
/**
|
|
512
|
-
|
|
511
|
+
/**
|
|
512
|
+
* `public/` altı dizin adları. `fonts` varsayılan: CDN CSS'teki
|
|
513
|
+
* `/fonts/…` rewrite'ı upload nesnesiyle hizalansın.
|
|
514
|
+
*/
|
|
515
|
+
include: /** @type {string[]} */ (["assets", "fonts"]),
|
|
513
516
|
cacheControl: "public, max-age=31536000, immutable",
|
|
517
|
+
/**
|
|
518
|
+
* Upload sırasında PutBucketCors ile yazılan `AllowedOrigin`.
|
|
519
|
+
* `null` / `false` → CORS adımı atlanır. Bucket'taki önceki CORS
|
|
520
|
+
* kuralının üzerine yazar — paylaşımlı kovada bilinçli açın.
|
|
521
|
+
* CDN CSS + aynı host font için genelde gerekmez.
|
|
522
|
+
*/
|
|
523
|
+
corsOrigin: /** @type {string | null} */ ("*"),
|
|
514
524
|
},
|
|
515
525
|
},
|
|
516
526
|
};
|
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.
|
|
@@ -1264,11 +1265,25 @@ export function normalizeBuildAssets(raw) {
|
|
|
1264
1265
|
}
|
|
1265
1266
|
}
|
|
1266
1267
|
|
|
1268
|
+
// CDN CSS `/fonts/…` → prefix'li URL yazar; fontlar upload'ta yoksa 403.
|
|
1269
|
+
// Kullanıcı `include: ["assets"]` yazsa bile prefix varken `fonts` eklenir.
|
|
1270
|
+
if (prefix && !include.includes("fonts")) {
|
|
1271
|
+
include = [...include, "fonts"];
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1267
1274
|
const cacheControl =
|
|
1268
1275
|
typeof uploadRaw.cacheControl === "string" && uploadRaw.cacheControl.trim()
|
|
1269
1276
|
? uploadRaw.cacheControl.trim()
|
|
1270
1277
|
: DEFAULT_BUILD.assets.upload.cacheControl;
|
|
1271
1278
|
|
|
1279
|
+
/** @type {string | null} */
|
|
1280
|
+
let corsOrigin = DEFAULT_BUILD.assets.upload.corsOrigin;
|
|
1281
|
+
if (uploadRaw.corsOrigin === null || uploadRaw.corsOrigin === false) {
|
|
1282
|
+
corsOrigin = null;
|
|
1283
|
+
} else if (typeof uploadRaw.corsOrigin === "string") {
|
|
1284
|
+
corsOrigin = uploadRaw.corsOrigin.trim() || null;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1272
1287
|
// Config'te bucket varsa `enabled` unutulmuş olsa bile aç. Açık `false` ezer.
|
|
1273
1288
|
const enabled =
|
|
1274
1289
|
uploadRaw.enabled === true ||
|
|
@@ -1284,6 +1299,7 @@ export function normalizeBuildAssets(raw) {
|
|
|
1284
1299
|
endpoint,
|
|
1285
1300
|
include,
|
|
1286
1301
|
cacheControl,
|
|
1302
|
+
corsOrigin,
|
|
1287
1303
|
},
|
|
1288
1304
|
};
|
|
1289
1305
|
}
|
|
@@ -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>}
|
|
@@ -458,9 +458,19 @@ export declare const DEFAULT_BUILD: {
|
|
|
458
458
|
keyPrefix: string;
|
|
459
459
|
region: string;
|
|
460
460
|
endpoint: null;
|
|
461
|
-
/**
|
|
461
|
+
/**
|
|
462
|
+
* `public/` altı dizin adları. `fonts` varsayılan: CDN CSS'teki
|
|
463
|
+
* `/fonts/…` rewrite'ı upload nesnesiyle hizalansın.
|
|
464
|
+
*/
|
|
462
465
|
include: string[];
|
|
463
466
|
cacheControl: string;
|
|
467
|
+
/**
|
|
468
|
+
* Upload sırasında PutBucketCors ile yazılan `AllowedOrigin`.
|
|
469
|
+
* `null` / `false` → CORS adımı atlanır. Bucket'taki önceki CORS
|
|
470
|
+
* kuralının üzerine yazar — paylaşımlı kovada bilinçli açın.
|
|
471
|
+
* CDN CSS + aynı host font için genelde gerekmez.
|
|
472
|
+
*/
|
|
473
|
+
corsOrigin: string | null;
|
|
464
474
|
};
|
|
465
475
|
};
|
|
466
476
|
};
|
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>}
|