jskelet 0.2.3 → 0.2.5

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.
Files changed (91) hide show
  1. package/AGENTS.md +132 -132
  2. package/CHANGELOG.md +15 -0
  3. package/LICENSE +21 -21
  4. package/bin/jskelet.mjs +103 -103
  5. package/docs/01-baslangic.md +285 -285
  6. package/docs/02-mimari.md +287 -287
  7. package/docs/03-routing.md +480 -480
  8. package/docs/04-render-ve-sablonlar.md +490 -490
  9. package/docs/05-islands.md +482 -482
  10. package/docs/06-cache.md +1209 -1202
  11. package/docs/08-build.md +366 -366
  12. package/docs/09-dev-araclari.md +335 -335
  13. package/docs/10-dagitim.md +329 -329
  14. package/docs/12-panel-ve-oturum.md +384 -384
  15. package/docs/README.md +105 -105
  16. package/docs/en/01-getting-started.md +292 -292
  17. package/docs/en/02-architecture.md +305 -305
  18. package/docs/en/03-routing.md +497 -497
  19. package/docs/en/04-rendering.md +504 -504
  20. package/docs/en/05-islands.md +492 -492
  21. package/docs/en/06-caching.md +1239 -1232
  22. package/docs/en/07-configuration.md +986 -986
  23. package/docs/en/08-build.md +383 -383
  24. package/docs/en/09-dev-tools.md +342 -342
  25. package/docs/en/10-deployment.md +332 -332
  26. package/docs/en/11-migration.md +359 -359
  27. package/docs/en/12-dashboards-and-sessions.md +392 -392
  28. package/docs/en/README.md +112 -112
  29. package/package.json +102 -102
  30. package/src/build/ensure-build.mjs +15 -15
  31. package/src/build/paths.mjs +143 -143
  32. package/src/build/resolve-peer.mjs +36 -36
  33. package/src/build/tasks/client.mjs +268 -268
  34. package/src/build/tasks/css.mjs +124 -124
  35. package/src/build/tasks/fonts.mjs +146 -146
  36. package/src/build/tasks/icons.mjs +224 -224
  37. package/src/build/tasks/images.mjs +244 -244
  38. package/src/build/tasks/precompress.mjs +78 -78
  39. package/src/client/cache-panel/i18n.js +670 -0
  40. package/src/client/cache-panel/login.html +74 -71
  41. package/src/client/cache-panel/panel.css +756 -740
  42. package/src/client/cache-panel/panel.html +308 -307
  43. package/src/client/cache-panel/panel.js +915 -808
  44. package/src/client/devtools/report.html +185 -185
  45. package/src/client/devtools/report.js +725 -725
  46. package/src/client/dom.js +95 -95
  47. package/src/client/form.js +192 -192
  48. package/src/client/index.js +35 -35
  49. package/src/client/registry.js +297 -297
  50. package/src/client/safe-image.js +91 -91
  51. package/src/client/store.js +36 -36
  52. package/src/client/swap.js +188 -188
  53. package/src/config/pattern.js +107 -107
  54. package/src/http/control-flow.js +71 -71
  55. package/src/http/cookies.js +257 -257
  56. package/src/http/request-cache.js +46 -46
  57. package/src/http/request-context.js +162 -162
  58. package/src/index.js +83 -83
  59. package/src/init.mjs +221 -221
  60. package/src/runtime/alias-hooks.mjs +119 -119
  61. package/src/runtime/register.mjs +4 -4
  62. package/src/server/assets.js +147 -147
  63. package/src/server/cache-deps.js +42 -42
  64. package/src/server/cache-panel.js +759 -738
  65. package/src/server/cloudflare.js +607 -595
  66. package/src/server/create-app.js +291 -291
  67. package/src/server/data-cache.js +462 -462
  68. package/src/server/dev/report.js +369 -369
  69. package/src/server/dev/socket.js +170 -170
  70. package/src/server/dev/version-check.mjs +139 -139
  71. package/src/server/html-cache.js +817 -817
  72. package/src/server/metadata.js +102 -102
  73. package/src/server/middleware/compression.js +205 -205
  74. package/src/server/middleware/csrf.js +134 -134
  75. package/src/server/middleware/dev-gate.js +62 -62
  76. package/src/server/middleware/headers.js +37 -37
  77. package/src/server/middleware/redirects.js +32 -32
  78. package/src/server/middleware/static-precompressed.js +100 -100
  79. package/src/server/middleware/upstream-proxy.js +141 -141
  80. package/src/server/prewarm.js +601 -601
  81. package/src/server/redis.js +569 -569
  82. package/src/server/router.js +128 -128
  83. package/src/server/status-page.js +164 -164
  84. package/src/server/upstream-limiter.js +376 -376
  85. package/src/server/upstream-tracking.js +166 -166
  86. package/src/start.mjs +7 -7
  87. package/src/templates/layout.ejs +44 -44
  88. package/src/version.mjs +31 -31
  89. package/src/views/components/loader.js +85 -85
  90. package/src/views/helpers/html.js +102 -102
  91. package/src/views/helpers/tags.js +245 -245
@@ -1,124 +1,124 @@
1
- /**
2
- * Tailwind v4 → tek statik stylesheet.
3
- *
4
- * Ayrı bir "critical CSS" üretilmez. Ölçümde inline kritik CSS ilk ekranı tam
5
- * kapsamadığı için sheet gelince sayfa yeniden akıyordu (bir liste sayfasında
6
- * CLS 0.307) ve aynı ~27 KB her HTML yanıtında tekrar ediyordu. Sıkıştırılmış
7
- * tek sheet'i render-blocking bırakmak hem daha hızlı hem CLS'siz; ikinci
8
- * ziyarette zaten immutable önbellekten geliyor.
9
- *
10
- * Tailwind'in sınıf taraması `globals.css` içindeki `@source` direktiflerine
11
- * bağlıdır. Otomatik tespit yalnızca stylesheet'in bulunduğu dizini tarar,
12
- * bu yüzden şablonlarda geçen varyantlar (`data-[active=false]:…`) sessizce
13
- * düşer. Yeni bir üst dizin eklediğinizde `@source` satırını da ekleyin.
14
- */
15
- import fs from "node:fs";
16
- import path from "node:path";
17
- import { patchManifest, pruneAssets, writeAsset } from "../paths.mjs";
18
- import { importFromApp, tryImportFromApp } from "../resolve-peer.mjs";
19
- import * as log from "../../log.mjs";
20
-
21
- /**
22
- * PostCSS boru hattı bir kez kurulur: Tailwind'in kendi önbelleği plugin
23
- * örneğinde yaşıyor, her derlemede yeniden oluşturmak watch turlarını
24
- * belirgin şekilde yavaşlatıyor.
25
- *
26
- * @param {string} root
27
- * @returns {Promise<(inputPath: string) => Promise<string>>}
28
- */
29
- async function createCompiler(root) {
30
- const [{ default: postcss }, { default: tailwindcss }] = await Promise.all([
31
- importFromApp(root, "postcss"),
32
- importFromApp(root, "@tailwindcss/postcss"),
33
- ]);
34
-
35
- // lightningcss opsiyonel: yoksa Tailwind'in kendi çıktısı kullanılır,
36
- // yalnızca birkaç kB daha büyük olur.
37
- const lightning = await tryImportFromApp(root, "lightningcss");
38
- const processor = postcss([tailwindcss()]);
39
-
40
- return async (inputPath) => {
41
- const css = fs.readFileSync(inputPath, "utf8");
42
- const result = await processor.process(css, { from: inputPath, to: inputPath });
43
-
44
- if (!lightning) return result.css;
45
-
46
- const minified = lightning.transform({
47
- filename: path.basename(inputPath),
48
- code: Buffer.from(result.css),
49
- minify: true,
50
- });
51
-
52
- return minified.code.toString("utf8");
53
- };
54
- }
55
-
56
- /**
57
- * @param {import('../../config/index.js').ResolvedConfig} config
58
- * @param {{ watch?: boolean }} [options]
59
- * @returns {Promise<Record<string, string>>}
60
- */
61
- export async function buildCss(config, { watch = false } = {}) {
62
- const input = config.dirs.styles;
63
- const compile = await createCompiler(config.root);
64
-
65
- const run = async () => {
66
- const started = Date.now();
67
- pruneAssets(["app."]);
68
- const css = await compile(input);
69
- const url = writeAsset("app.css", css);
70
- return { url, bytes: Buffer.byteLength(css), elapsed: Date.now() - started };
71
- };
72
-
73
- const first = await run();
74
- log.detail(log.size(first.bytes));
75
-
76
- if (watch) {
77
- watchCssSources(config, async () => {
78
- try {
79
- const result = await run();
80
- // Yeni hash manifest'e yazılmazsa HTML silinmiş dosyayı ister.
81
- patchManifest("app.css", result.url);
82
- log.event({
83
- scope: "css",
84
- message: "rebuilt",
85
- note: log.size(result.bytes),
86
- time: result.elapsed,
87
- });
88
- } catch (error) {
89
- log.event({
90
- symbol: log.symbols.fail,
91
- scope: "css",
92
- message: "failed",
93
- note: error.message,
94
- });
95
- }
96
- });
97
- }
98
-
99
- return { "app.css": first.url };
100
- }
101
-
102
- /**
103
- * Şablon ve island dosyaları da izlenir: Tailwind sınıfları oradan geliyor,
104
- * yalnızca `styles/` izlemek yeni bir utility yazıldığında rebuild etmez.
105
- *
106
- * @param {import('../../config/index.js').ResolvedConfig} config
107
- * @param {() => void} onChange
108
- */
109
- function watchCssSources(config, onChange) {
110
- const targets = [
111
- path.dirname(config.dirs.styles),
112
- config.dirs.views,
113
- config.dirs.client,
114
- ];
115
-
116
- let timer = null;
117
- for (const target of targets) {
118
- if (!fs.existsSync(target)) continue;
119
- fs.watch(target, { recursive: true }, () => {
120
- clearTimeout(timer);
121
- timer = setTimeout(onChange, 120);
122
- });
123
- }
124
- }
1
+ /**
2
+ * Tailwind v4 → tek statik stylesheet.
3
+ *
4
+ * Ayrı bir "critical CSS" üretilmez. Ölçümde inline kritik CSS ilk ekranı tam
5
+ * kapsamadığı için sheet gelince sayfa yeniden akıyordu (bir liste sayfasında
6
+ * CLS 0.307) ve aynı ~27 KB her HTML yanıtında tekrar ediyordu. Sıkıştırılmış
7
+ * tek sheet'i render-blocking bırakmak hem daha hızlı hem CLS'siz; ikinci
8
+ * ziyarette zaten immutable önbellekten geliyor.
9
+ *
10
+ * Tailwind'in sınıf taraması `globals.css` içindeki `@source` direktiflerine
11
+ * bağlıdır. Otomatik tespit yalnızca stylesheet'in bulunduğu dizini tarar,
12
+ * bu yüzden şablonlarda geçen varyantlar (`data-[active=false]:…`) sessizce
13
+ * düşer. Yeni bir üst dizin eklediğinizde `@source` satırını da ekleyin.
14
+ */
15
+ import fs from "node:fs";
16
+ import path from "node:path";
17
+ import { patchManifest, pruneAssets, writeAsset } from "../paths.mjs";
18
+ import { importFromApp, tryImportFromApp } from "../resolve-peer.mjs";
19
+ import * as log from "../../log.mjs";
20
+
21
+ /**
22
+ * PostCSS boru hattı bir kez kurulur: Tailwind'in kendi önbelleği plugin
23
+ * örneğinde yaşıyor, her derlemede yeniden oluşturmak watch turlarını
24
+ * belirgin şekilde yavaşlatıyor.
25
+ *
26
+ * @param {string} root
27
+ * @returns {Promise<(inputPath: string) => Promise<string>>}
28
+ */
29
+ async function createCompiler(root) {
30
+ const [{ default: postcss }, { default: tailwindcss }] = await Promise.all([
31
+ importFromApp(root, "postcss"),
32
+ importFromApp(root, "@tailwindcss/postcss"),
33
+ ]);
34
+
35
+ // lightningcss opsiyonel: yoksa Tailwind'in kendi çıktısı kullanılır,
36
+ // yalnızca birkaç kB daha büyük olur.
37
+ const lightning = await tryImportFromApp(root, "lightningcss");
38
+ const processor = postcss([tailwindcss()]);
39
+
40
+ return async (inputPath) => {
41
+ const css = fs.readFileSync(inputPath, "utf8");
42
+ const result = await processor.process(css, { from: inputPath, to: inputPath });
43
+
44
+ if (!lightning) return result.css;
45
+
46
+ const minified = lightning.transform({
47
+ filename: path.basename(inputPath),
48
+ code: Buffer.from(result.css),
49
+ minify: true,
50
+ });
51
+
52
+ return minified.code.toString("utf8");
53
+ };
54
+ }
55
+
56
+ /**
57
+ * @param {import('../../config/index.js').ResolvedConfig} config
58
+ * @param {{ watch?: boolean }} [options]
59
+ * @returns {Promise<Record<string, string>>}
60
+ */
61
+ export async function buildCss(config, { watch = false } = {}) {
62
+ const input = config.dirs.styles;
63
+ const compile = await createCompiler(config.root);
64
+
65
+ const run = async () => {
66
+ const started = Date.now();
67
+ pruneAssets(["app."]);
68
+ const css = await compile(input);
69
+ const url = writeAsset("app.css", css);
70
+ return { url, bytes: Buffer.byteLength(css), elapsed: Date.now() - started };
71
+ };
72
+
73
+ const first = await run();
74
+ log.detail(log.size(first.bytes));
75
+
76
+ if (watch) {
77
+ watchCssSources(config, async () => {
78
+ try {
79
+ const result = await run();
80
+ // Yeni hash manifest'e yazılmazsa HTML silinmiş dosyayı ister.
81
+ patchManifest("app.css", result.url);
82
+ log.event({
83
+ scope: "css",
84
+ message: "rebuilt",
85
+ note: log.size(result.bytes),
86
+ time: result.elapsed,
87
+ });
88
+ } catch (error) {
89
+ log.event({
90
+ symbol: log.symbols.fail,
91
+ scope: "css",
92
+ message: "failed",
93
+ note: error.message,
94
+ });
95
+ }
96
+ });
97
+ }
98
+
99
+ return { "app.css": first.url };
100
+ }
101
+
102
+ /**
103
+ * Şablon ve island dosyaları da izlenir: Tailwind sınıfları oradan geliyor,
104
+ * yalnızca `styles/` izlemek yeni bir utility yazıldığında rebuild etmez.
105
+ *
106
+ * @param {import('../../config/index.js').ResolvedConfig} config
107
+ * @param {() => void} onChange
108
+ */
109
+ function watchCssSources(config, onChange) {
110
+ const targets = [
111
+ path.dirname(config.dirs.styles),
112
+ config.dirs.views,
113
+ config.dirs.client,
114
+ ];
115
+
116
+ let timer = null;
117
+ for (const target of targets) {
118
+ if (!fs.existsSync(target)) continue;
119
+ fs.watch(target, { recursive: true }, () => {
120
+ clearTimeout(timer);
121
+ timer = setTimeout(onChange, 120);
122
+ });
123
+ }
124
+ }
@@ -1,146 +1,146 @@
1
- /**
2
- * `next/font/google` yerine self-host font dosyaları.
3
- *
4
- * Dosyalar `public/fonts/` altında **sabit isimlerle** durur (hash yok), çünkü
5
- * `@font-face` içindeki `url()` yolları elle yazılıyor; hash'lemek her build'de
6
- * stylesheet'i de değiştirmek zorunda bırakırdı.
7
- *
8
- * Dosya yoksa bir kez Google Fonts'tan indirilir ve **commit edilmesi
9
- * beklenir**: build'in ağa bağımlı olması CI'da kırılgan. İndirme başarısız
10
- * olursa uyarı basılır ve sayfa sistem font yığınına düşer — build durmaz.
11
- *
12
- * Yapılandırma:
13
- * fonts: [{ family: "Inter", weights: [400, 700] }]
14
- * Çıktı: `public/fonts/inter-400.woff2`, manifest anahtarı aynı dosya adı.
15
- */
16
- import fs from "node:fs";
17
- import path from "node:path";
18
- import * as log from "../../log.mjs";
19
-
20
- /** woff2 döndürmesi için modern tarayıcı UA'sı gerekir. */
21
- const UA =
22
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
23
- "(KHTML, like Gecko) Chrome/124.0 Safari/537.36";
24
-
25
- /**
26
- * @param {string} family
27
- * @returns {string}
28
- */
29
- function toSlug(family) {
30
- return family.trim().toLowerCase().replace(/\s+/g, "-");
31
- }
32
-
33
- /**
34
- * @param {string} family
35
- * @param {number[]} weights
36
- * @returns {string}
37
- */
38
- function cssUrl(family, weights) {
39
- const name = family.trim().replace(/\s+/g, "+");
40
- const wght = [...weights].sort((a, b) => a - b).join(";");
41
- return `https://fonts.googleapis.com/css2?family=${name}:wght@${wght}&display=swap`;
42
- }
43
-
44
- /**
45
- * @param {import('../../config/index.js').ResolvedConfig} config
46
- * @returns {Promise<Record<string, string>>}
47
- */
48
- export async function copyFonts(config) {
49
- const fontsDir = config.dirs.fonts;
50
- fs.mkdirSync(fontsDir, { recursive: true });
51
-
52
- /** @type {Record<string, string>} */
53
- const manifest = {};
54
- let expected = 0;
55
-
56
- for (const family of config.fonts) {
57
- const slug = family.slug ?? toSlug(family.family);
58
- const weights = family.weights ?? [400];
59
- expected += weights.length;
60
-
61
- /** @type {number[]} */
62
- const missing = [];
63
-
64
- for (const weight of weights) {
65
- const fileName = `${slug}-${weight}.woff2`;
66
- if (fs.existsSync(path.join(fontsDir, fileName))) {
67
- manifest[fileName] = `/fonts/${fileName}`;
68
- continue;
69
- }
70
- missing.push(weight);
71
- }
72
-
73
- if (!missing.length) continue;
74
-
75
- const downloaded = await downloadFromGoogle(
76
- cssUrl(family.family, missing),
77
- missing,
78
- );
79
-
80
- for (const [weight, buffer] of downloaded) {
81
- const fileName = `${slug}-${weight}.woff2`;
82
- fs.writeFileSync(path.join(fontsDir, fileName), buffer);
83
- manifest[fileName] = `/fonts/${fileName}`;
84
- log.detail(`${fileName} downloaded (${log.size(buffer.byteLength)})`);
85
- }
86
-
87
- const stillMissing = missing.filter(
88
- (weight) => !manifest[`${slug}-${weight}.woff2`],
89
- );
90
- if (stillMissing.length) {
91
- log.warn(
92
- `${family.family} ${stillMissing.join(", ")} could not be downloaded — falling back to the system font stack.`,
93
- );
94
- }
95
- }
96
-
97
- log.summary(`${Object.keys(manifest).length}/${expected} weights`);
98
- return manifest;
99
- }
100
-
101
- /**
102
- * Google'ın döndürdüğü CSS'te her ağırlık ve her subset için ayrı bir
103
- * `@font-face` bloğu var. Latin subset'i (`U+0000-00FF`) seçilir: diğerleri
104
- * çoğu site için ölü ağırlık ve `unicode-range` olmadan hepsini indirmek
105
- * font boyutunu katlar.
106
- *
107
- * @param {string} url
108
- * @param {number[]} weights
109
- * @returns {Promise<[number, Buffer][]>}
110
- */
111
- async function downloadFromGoogle(url, weights) {
112
- /** @type {[number, Buffer][]} */
113
- const out = [];
114
-
115
- let css;
116
- try {
117
- const response = await fetch(url, { headers: { "User-Agent": UA } });
118
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
119
- css = await response.text();
120
- } catch (error) {
121
- log.warn(`could not fetch Google Fonts CSS (${error.message})`);
122
- return out;
123
- }
124
-
125
- const blocks = css.split("@font-face").slice(1);
126
-
127
- for (const weight of weights) {
128
- const block = blocks.find(
129
- (candidate) =>
130
- candidate.includes(`font-weight: ${weight}`) &&
131
- candidate.includes("U+0000-00FF"),
132
- );
133
- const fileUrl = block?.match(/url\((https:[^)]+\.woff2)\)/)?.[1];
134
- if (!fileUrl) continue;
135
-
136
- try {
137
- const response = await fetch(fileUrl, { headers: { "User-Agent": UA } });
138
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
139
- out.push([weight, Buffer.from(await response.arrayBuffer())]);
140
- } catch (error) {
141
- log.warn(`${weight} could not be downloaded (${error.message})`);
142
- }
143
- }
144
-
145
- return out;
146
- }
1
+ /**
2
+ * `next/font/google` yerine self-host font dosyaları.
3
+ *
4
+ * Dosyalar `public/fonts/` altında **sabit isimlerle** durur (hash yok), çünkü
5
+ * `@font-face` içindeki `url()` yolları elle yazılıyor; hash'lemek her build'de
6
+ * stylesheet'i de değiştirmek zorunda bırakırdı.
7
+ *
8
+ * Dosya yoksa bir kez Google Fonts'tan indirilir ve **commit edilmesi
9
+ * beklenir**: build'in ağa bağımlı olması CI'da kırılgan. İndirme başarısız
10
+ * olursa uyarı basılır ve sayfa sistem font yığınına düşer — build durmaz.
11
+ *
12
+ * Yapılandırma:
13
+ * fonts: [{ family: "Inter", weights: [400, 700] }]
14
+ * Çıktı: `public/fonts/inter-400.woff2`, manifest anahtarı aynı dosya adı.
15
+ */
16
+ import fs from "node:fs";
17
+ import path from "node:path";
18
+ import * as log from "../../log.mjs";
19
+
20
+ /** woff2 döndürmesi için modern tarayıcı UA'sı gerekir. */
21
+ const UA =
22
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
23
+ "(KHTML, like Gecko) Chrome/124.0 Safari/537.36";
24
+
25
+ /**
26
+ * @param {string} family
27
+ * @returns {string}
28
+ */
29
+ function toSlug(family) {
30
+ return family.trim().toLowerCase().replace(/\s+/g, "-");
31
+ }
32
+
33
+ /**
34
+ * @param {string} family
35
+ * @param {number[]} weights
36
+ * @returns {string}
37
+ */
38
+ function cssUrl(family, weights) {
39
+ const name = family.trim().replace(/\s+/g, "+");
40
+ const wght = [...weights].sort((a, b) => a - b).join(";");
41
+ return `https://fonts.googleapis.com/css2?family=${name}:wght@${wght}&display=swap`;
42
+ }
43
+
44
+ /**
45
+ * @param {import('../../config/index.js').ResolvedConfig} config
46
+ * @returns {Promise<Record<string, string>>}
47
+ */
48
+ export async function copyFonts(config) {
49
+ const fontsDir = config.dirs.fonts;
50
+ fs.mkdirSync(fontsDir, { recursive: true });
51
+
52
+ /** @type {Record<string, string>} */
53
+ const manifest = {};
54
+ let expected = 0;
55
+
56
+ for (const family of config.fonts) {
57
+ const slug = family.slug ?? toSlug(family.family);
58
+ const weights = family.weights ?? [400];
59
+ expected += weights.length;
60
+
61
+ /** @type {number[]} */
62
+ const missing = [];
63
+
64
+ for (const weight of weights) {
65
+ const fileName = `${slug}-${weight}.woff2`;
66
+ if (fs.existsSync(path.join(fontsDir, fileName))) {
67
+ manifest[fileName] = `/fonts/${fileName}`;
68
+ continue;
69
+ }
70
+ missing.push(weight);
71
+ }
72
+
73
+ if (!missing.length) continue;
74
+
75
+ const downloaded = await downloadFromGoogle(
76
+ cssUrl(family.family, missing),
77
+ missing,
78
+ );
79
+
80
+ for (const [weight, buffer] of downloaded) {
81
+ const fileName = `${slug}-${weight}.woff2`;
82
+ fs.writeFileSync(path.join(fontsDir, fileName), buffer);
83
+ manifest[fileName] = `/fonts/${fileName}`;
84
+ log.detail(`${fileName} downloaded (${log.size(buffer.byteLength)})`);
85
+ }
86
+
87
+ const stillMissing = missing.filter(
88
+ (weight) => !manifest[`${slug}-${weight}.woff2`],
89
+ );
90
+ if (stillMissing.length) {
91
+ log.warn(
92
+ `${family.family} ${stillMissing.join(", ")} could not be downloaded — falling back to the system font stack.`,
93
+ );
94
+ }
95
+ }
96
+
97
+ log.summary(`${Object.keys(manifest).length}/${expected} weights`);
98
+ return manifest;
99
+ }
100
+
101
+ /**
102
+ * Google'ın döndürdüğü CSS'te her ağırlık ve her subset için ayrı bir
103
+ * `@font-face` bloğu var. Latin subset'i (`U+0000-00FF`) seçilir: diğerleri
104
+ * çoğu site için ölü ağırlık ve `unicode-range` olmadan hepsini indirmek
105
+ * font boyutunu katlar.
106
+ *
107
+ * @param {string} url
108
+ * @param {number[]} weights
109
+ * @returns {Promise<[number, Buffer][]>}
110
+ */
111
+ async function downloadFromGoogle(url, weights) {
112
+ /** @type {[number, Buffer][]} */
113
+ const out = [];
114
+
115
+ let css;
116
+ try {
117
+ const response = await fetch(url, { headers: { "User-Agent": UA } });
118
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
119
+ css = await response.text();
120
+ } catch (error) {
121
+ log.warn(`could not fetch Google Fonts CSS (${error.message})`);
122
+ return out;
123
+ }
124
+
125
+ const blocks = css.split("@font-face").slice(1);
126
+
127
+ for (const weight of weights) {
128
+ const block = blocks.find(
129
+ (candidate) =>
130
+ candidate.includes(`font-weight: ${weight}`) &&
131
+ candidate.includes("U+0000-00FF"),
132
+ );
133
+ const fileUrl = block?.match(/url\((https:[^)]+\.woff2)\)/)?.[1];
134
+ if (!fileUrl) continue;
135
+
136
+ try {
137
+ const response = await fetch(fileUrl, { headers: { "User-Agent": UA } });
138
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
139
+ out.push([weight, Buffer.from(await response.arrayBuffer())]);
140
+ } catch (error) {
141
+ log.warn(`${weight} could not be downloaded (${error.message})`);
142
+ }
143
+ }
144
+
145
+ return out;
146
+ }