jskelet 0.1.1
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/AGENTS.md +127 -0
- package/CHANGELOG.md +40 -0
- package/LICENSE +21 -0
- package/README.md +342 -0
- package/bin/jskelet.mjs +104 -0
- package/docs/01-baslangic.md +285 -0
- package/docs/02-mimari.md +287 -0
- package/docs/03-routing.md +437 -0
- package/docs/04-render-ve-sablonlar.md +490 -0
- package/docs/05-islands.md +429 -0
- package/docs/06-cache.md +409 -0
- package/docs/07-yapilandirma.md +673 -0
- package/docs/08-build.md +366 -0
- package/docs/09-dev-araclari.md +302 -0
- package/docs/10-dagitim.md +329 -0
- package/docs/11-tasima.md +352 -0
- package/docs/README.md +82 -0
- package/package.json +97 -0
- package/src/build/build.mjs +138 -0
- package/src/build/ensure-build.mjs +15 -0
- package/src/build/paths.mjs +118 -0
- package/src/build/resolve-peer.mjs +36 -0
- package/src/build/tasks/client.mjs +268 -0
- package/src/build/tasks/css.mjs +124 -0
- package/src/build/tasks/fonts.mjs +146 -0
- package/src/build/tasks/icons.mjs +224 -0
- package/src/build/tasks/images.mjs +244 -0
- package/src/build/tasks/precompress.mjs +78 -0
- package/src/client/devtools/overlay.js +1763 -0
- package/src/client/devtools/report.html +185 -0
- package/src/client/devtools/report.js +712 -0
- package/src/client/dom.js +95 -0
- package/src/client/index.js +26 -0
- package/src/client/registry.js +223 -0
- package/src/client/safe-image.js +91 -0
- package/src/client/store.js +36 -0
- package/src/config/defaults.js +102 -0
- package/src/config/index.js +433 -0
- package/src/config/pattern.js +107 -0
- package/src/dev-server.mjs +383 -0
- package/src/http/control-flow.js +56 -0
- package/src/http/request-cache.js +46 -0
- package/src/index.js +35 -0
- package/src/init.mjs +220 -0
- package/src/log.mjs +332 -0
- package/src/logo.png +0 -0
- package/src/runtime/alias-hooks.mjs +119 -0
- package/src/runtime/register.mjs +4 -0
- package/src/server/assets.js +119 -0
- package/src/server/create-app.js +167 -0
- package/src/server/dev/devtools.js +383 -0
- package/src/server/dev/report.js +351 -0
- package/src/server/head-hints.js +132 -0
- package/src/server/html-cache.js +166 -0
- package/src/server/metadata.js +102 -0
- package/src/server/middleware/compression.js +205 -0
- package/src/server/middleware/dev-gate.js +62 -0
- package/src/server/middleware/headers.js +37 -0
- package/src/server/middleware/redirects.js +32 -0
- package/src/server/middleware/static-precompressed.js +100 -0
- package/src/server/middleware/upstream-proxy.js +141 -0
- package/src/server/prewarm.js +283 -0
- package/src/server/render.js +356 -0
- package/src/server/router.js +121 -0
- package/src/server/status-page.js +164 -0
- package/src/server/upstream-tracking.js +51 -0
- package/src/start.mjs +7 -0
- package/src/templates/layout.ejs +44 -0
- package/src/version.mjs +17 -0
- package/src/views/components/loader.js +85 -0
- package/src/views/helpers/html.js +102 -0
- package/src/views/helpers/tags.js +193 -0
package/docs/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# JSkelet belgeleri
|
|
2
|
+
|
|
3
|
+
JSkelet, SEO ve hız odaklı siteler için "framework'süz hissettiren" bir
|
|
4
|
+
framework: Express 5 + EJS ile sunucuda tam HTML üretir, etkileşimi vanilla JS
|
|
5
|
+
island'larla ekler, CSS'i Tailwind v4 ile tek bir stylesheet'e derler ve ISR
|
|
6
|
+
yerine süreç belleğinde yaşayan, stale-while-revalidate'li bir HTML TTL cache
|
|
7
|
+
kullanır. React yok, TypeScript yok; düz JavaScript ve JSDoc.
|
|
8
|
+
|
|
9
|
+
Bu dizin framework'ün tam referansıdır. Sıralı okumak için baştan başlayın;
|
|
10
|
+
belirli bir konuyu arıyorsanız doğrudan ilgili başlığa gidin.
|
|
11
|
+
|
|
12
|
+
## Sıralı okuma
|
|
13
|
+
|
|
14
|
+
| Belge | Konu |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| [01-baslangic.md](./01-baslangic.md) | Kurulum, `jskelet init`, ilk route, ilk island, dizin yapısı, CLI komutları |
|
|
17
|
+
| [02-mimari.md](./02-mimari.md) | Mimari kararlar ve gerekçeleri: island modeli, tam sunucu HTML'i, cache stratejisi, middleware sırası |
|
|
18
|
+
| [03-routing.md](./03-routing.md) | Route modülü sözleşmesi, yükleme sırası, controller sözleşmesi, `ctx`, `notFound`/`redirect`, config redirects/rewrites |
|
|
19
|
+
| [04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md) | EJS layout, sayfalar, otomatik bileşen kaydı, `html`/`tags` yardımcıları, metadata → `<head>`, hook'lar |
|
|
20
|
+
| [05-islands.md](./05-islands.md) | `data-island` sözleşmesi, hidrasyon stratejileri, `client/entries/*`, `createStore`, DOM yardımcıları, `startSafeImages` |
|
|
21
|
+
| [06-cache.md](./06-cache.md) | `withHtmlCache`, `revalidate`, stale-while-revalidate, cache anahtarı, `X-JSkelet-Cache`, istek içi cache, degraded render, prewarm |
|
|
22
|
+
| [07-yapilandirma.md](./07-yapilandirma.md) | `jskelet.config.mjs` tam referansı, `source` desen sözdizimi, ortam değişkenleri tablosu |
|
|
23
|
+
| [08-build.md](./08-build.md) | Build hattı, manifest, hash'li varlıklar, CSS/Tailwind `@source`, fontlar, ikon sprite, görsel optimizasyonu, precompress |
|
|
24
|
+
| [09-dev-araclari.md](./09-dev-araclari.md) | `jskelet dev` akışı, watch dizinleri, CSS hot-swap, devtools overlay (Alt+D), rapor sayfası, dev gate |
|
|
25
|
+
| [10-dagitim.md](./10-dagitim.md) | Prod build + start, ortam değişkenleri, Docker, ters proxy, sağlık kontrolü |
|
|
26
|
+
| [11-tasima.md](./11-tasima.md) | Next.js'ten taşıma: karşılık tablosu ve adım adım plan |
|
|
27
|
+
|
|
28
|
+
## Konuya göre hızlı erişim
|
|
29
|
+
|
|
30
|
+
- **Bir sayfa nasıl eklenir?** → [03-routing.md](./03-routing.md) ve
|
|
31
|
+
[04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md)
|
|
32
|
+
- **Bir butona tıklandığında bir şey olsun istiyorum** →
|
|
33
|
+
[05-islands.md](./05-islands.md)
|
|
34
|
+
- **Sayfa neden `MISS` dönüyor / neden eski veri görüyorum?** →
|
|
35
|
+
[06-cache.md](./06-cache.md)
|
|
36
|
+
- **Hangi config alanı ne yapıyor?** → [07-yapilandirma.md](./07-yapilandirma.md)
|
|
37
|
+
- **Stil çıkmıyor / ikon görünmüyor** → [08-build.md](./08-build.md)
|
|
38
|
+
- **Yayına alma** → [10-dagitim.md](./10-dagitim.md)
|
|
39
|
+
|
|
40
|
+
## Çalışan örnekler
|
|
41
|
+
|
|
42
|
+
Üçü de çalışır durumda; belgelerdeki örneklerin çoğu buralardan alınmıştır.
|
|
43
|
+
|
|
44
|
+
**`examples/minimal/`** — iki route, bir bileşen, bir island, minimal config.
|
|
45
|
+
Framework'ün en küçük çalışan hâli.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm --prefix examples/minimal install
|
|
49
|
+
npm --prefix examples/minimal run dev
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**`examples/blog/`** — dinamik route (`/blog/:slug`), etiket sayfaları,
|
|
53
|
+
`redirects`/`rewrites`/`headers`/`cache` yapılandırmasının tamamı, fragment ile
|
|
54
|
+
gelen sekme panelleri, form gönderimi, prewarm, `robots.txt`/`sitemap.xml`/`rss.xml`
|
|
55
|
+
ve dört island (tema, sekme, arama, form).
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm --prefix examples/blog install
|
|
59
|
+
npm --prefix examples/blog run dev
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**`examples/marketing/`** — framework'ün kendi tanıtım sitesi: hero, kıyaslama
|
|
63
|
+
tablosu, canlı gecikme ölçümü, SSS, belgeler dizini, sürüm notları ve indirme
|
|
64
|
+
sayfası. Sayfadaki bayt sayıları `lib/payload.js` içinde sitenin **kendi** build
|
|
65
|
+
çıktısından, sürüm künyesi ise `lib/release.js` içinde kurulu paketin
|
|
66
|
+
`package.json`'ından okunur; gecikme sayıları `latency` island'ında tarayıcıda
|
|
67
|
+
ölçülür. Uzun TTL (bir saat) ve tüm sayfaları ısıtan prewarm ile, cache'in en
|
|
68
|
+
verimli çalıştığı profili gösterir.
|
|
69
|
+
|
|
70
|
+
Site aynı zamanda **iki dilli**: varsayılan İngilizce kökte, Türkçe `/tr`
|
|
71
|
+
altında ve route adları iki dilde de aynı. Framework'te i18n yok; dil
|
|
72
|
+
çözümlemesi `lib/i18n.js` içinde uygulamanın kendi sözleşmesi olarak duruyor ve
|
|
73
|
+
`hooks.layoutContext` ile bir sözlüğe bağlanıyor. Çok dilli bir siteyi bu
|
|
74
|
+
yüzeyle nasıl kurabileceğinizi görmek için bakılacak yer burası.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm --prefix examples/marketing install
|
|
78
|
+
npm --prefix examples/marketing run dev
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Her üç örnekte `node smoke.mjs` sunucu ayaktayken uçların beklendiği gibi
|
|
82
|
+
yanıt verdiğini doğrular.
|
package/package.json
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jskelet",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "A framework that feels like no framework: Express 5 + EJS server rendering, vanilla JS islands, Tailwind v4 and an in-process HTML TTL cache.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Ayberk Enis",
|
|
8
|
+
"homepage": "https://github.com/ayberkenis/jskelet#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/ayberkenis/jskelet.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/ayberkenis/jskelet/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"ssr",
|
|
18
|
+
"islands",
|
|
19
|
+
"express",
|
|
20
|
+
"ejs",
|
|
21
|
+
"esbuild",
|
|
22
|
+
"tailwind",
|
|
23
|
+
"seo",
|
|
24
|
+
"isr"
|
|
25
|
+
],
|
|
26
|
+
"bin": {
|
|
27
|
+
"jskelet": "./bin/jskelet.mjs"
|
|
28
|
+
},
|
|
29
|
+
"main": "./src/index.js",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": "./src/index.js",
|
|
32
|
+
"./server": "./src/index.js",
|
|
33
|
+
"./client": "./src/client/index.js",
|
|
34
|
+
"./html": "./src/views/helpers/html.js",
|
|
35
|
+
"./tags": "./src/views/helpers/tags.js",
|
|
36
|
+
"./log": "./src/log.mjs",
|
|
37
|
+
"./register": "./src/runtime/register.mjs",
|
|
38
|
+
"./layout": "./src/templates/layout.ejs"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"bin",
|
|
42
|
+
"src",
|
|
43
|
+
"docs",
|
|
44
|
+
"README.md",
|
|
45
|
+
"AGENTS.md",
|
|
46
|
+
"CHANGELOG.md",
|
|
47
|
+
"LICENSE"
|
|
48
|
+
],
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=22"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"lint": "eslint",
|
|
54
|
+
"test": "node --test \"test/**/*.test.mjs\"",
|
|
55
|
+
"example:minimal": "npm --prefix examples/minimal run dev",
|
|
56
|
+
"example:blog": "npm --prefix examples/blog run dev"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"ejs": "^6.0.1",
|
|
60
|
+
"esbuild": "^0.28.2",
|
|
61
|
+
"express": "^5.2.1",
|
|
62
|
+
"tailwind-merge": "^3.5.0"
|
|
63
|
+
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"@phosphor-icons/core": "^2.1.1",
|
|
66
|
+
"@tailwindcss/postcss": "^4.3.3",
|
|
67
|
+
"lightningcss": "^1.32.0",
|
|
68
|
+
"postcss": "^8.5.26",
|
|
69
|
+
"sharp": "^0.35.4",
|
|
70
|
+
"tailwindcss": "^4.3.3"
|
|
71
|
+
},
|
|
72
|
+
"peerDependenciesMeta": {
|
|
73
|
+
"@phosphor-icons/core": {
|
|
74
|
+
"optional": true
|
|
75
|
+
},
|
|
76
|
+
"@tailwindcss/postcss": {
|
|
77
|
+
"optional": true
|
|
78
|
+
},
|
|
79
|
+
"lightningcss": {
|
|
80
|
+
"optional": true
|
|
81
|
+
},
|
|
82
|
+
"postcss": {
|
|
83
|
+
"optional": true
|
|
84
|
+
},
|
|
85
|
+
"sharp": {
|
|
86
|
+
"optional": true
|
|
87
|
+
},
|
|
88
|
+
"tailwindcss": {
|
|
89
|
+
"optional": true
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"devDependencies": {
|
|
93
|
+
"@eslint/js": "^9",
|
|
94
|
+
"eslint": "^9",
|
|
95
|
+
"globals": "^16"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tek build girişi: fontlar → ikon sprite → CSS → client JS → görseller →
|
|
3
|
+
* manifest → precompress.
|
|
4
|
+
*
|
|
5
|
+
* Sıra rastgele değil. CSS Tailwind için şablonları tarar, bu yüzden ikon
|
|
6
|
+
* sprite'ından sonra gelir (sprite bir varlık, sınıf üretmez ama manifest
|
|
7
|
+
* anahtarı verir). Precompress en sonda: sıkıştırılacak her şey üretilmiş
|
|
8
|
+
* olmalı.
|
|
9
|
+
*
|
|
10
|
+
* Görevler yalnızca ilgili yapılandırma varsa çalışır. Font tanımlamayan bir
|
|
11
|
+
* proje font adımını hiç görmez; bu, "framework her projeye kendi
|
|
12
|
+
* varsayımlarını dayatmaz" ilkesinin build tarafındaki karşılığı.
|
|
13
|
+
*/
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import process from "node:process";
|
|
17
|
+
import { loadConfig } from "../config/index.js";
|
|
18
|
+
import { initBuildPaths, paths, writeManifest } from "./paths.mjs";
|
|
19
|
+
import * as log from "../log.mjs";
|
|
20
|
+
import { FRAMEWORK_VERSION } from "../version.mjs";
|
|
21
|
+
|
|
22
|
+
const watch = process.argv.includes("--watch");
|
|
23
|
+
|
|
24
|
+
// Dev script'i banner'ı ve "Ready" özetini kendisi basar; alt süreçte
|
|
25
|
+
// yalnızca build satırları görünür.
|
|
26
|
+
const child = Boolean(process.env.JSKELET_CHILD);
|
|
27
|
+
|
|
28
|
+
const config = await loadConfig();
|
|
29
|
+
initBuildPaths(config);
|
|
30
|
+
|
|
31
|
+
const started = Date.now();
|
|
32
|
+
|
|
33
|
+
if (!child) {
|
|
34
|
+
log.banner(
|
|
35
|
+
`v${FRAMEWORK_VERSION}`,
|
|
36
|
+
watch ? "build · watch" : "production build",
|
|
37
|
+
config.root,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
log.section("build");
|
|
42
|
+
|
|
43
|
+
/** @type {Record<string, string>} */
|
|
44
|
+
const manifest = {};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @template T
|
|
48
|
+
* @param {string} label
|
|
49
|
+
* @param {() => Promise<T>} run
|
|
50
|
+
* @returns {Promise<T>}
|
|
51
|
+
*/
|
|
52
|
+
async function task(label, run) {
|
|
53
|
+
const step = log.task(label);
|
|
54
|
+
try {
|
|
55
|
+
const result = await run();
|
|
56
|
+
step.done();
|
|
57
|
+
return result;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
step.fail(error);
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (config.fonts?.length) {
|
|
65
|
+
const { copyFonts } = await import("./tasks/fonts.mjs");
|
|
66
|
+
Object.assign(manifest, await task("Fonts", () => copyFonts(config)));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (config.icons !== false) {
|
|
70
|
+
const { buildIconSprite } = await import("./tasks/icons.mjs");
|
|
71
|
+
Object.assign(manifest, await task("Icon sprite", () => buildIconSprite(config)));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (fs.existsSync(config.dirs.styles)) {
|
|
75
|
+
const { buildCss } = await import("./tasks/css.mjs");
|
|
76
|
+
Object.assign(manifest, await task("CSS", () => buildCss(config, { watch })));
|
|
77
|
+
} else {
|
|
78
|
+
log.warn(
|
|
79
|
+
`stylesheet girişi yok: ${path.relative(config.root, config.dirs.styles)} — CSS adımı atlandı`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const { buildClient } = await import("./tasks/client.mjs");
|
|
84
|
+
Object.assign(manifest, await task("Client JS", () => buildClient(config, { watch })));
|
|
85
|
+
|
|
86
|
+
// Görsel optimizasyonu `sharp` gerektirir ve watch turunda pahalı. Paket
|
|
87
|
+
// kurulu değilse adım sessizce atlanır: `image()` orijinal dosyaya döner,
|
|
88
|
+
// sayfa çalışmaya devam eder.
|
|
89
|
+
if (config.images !== false && !watch) {
|
|
90
|
+
const { tryImportFromApp } = await import("./resolve-peer.mjs");
|
|
91
|
+
const sharp = await tryImportFromApp(config.root, "sharp");
|
|
92
|
+
if (sharp) {
|
|
93
|
+
const { buildImages } = await import("./tasks/images.mjs");
|
|
94
|
+
await task("Images", () => buildImages(config, sharp.default));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
writeManifest(manifest);
|
|
99
|
+
|
|
100
|
+
// Watch modunda her değişiklikte kalite-11 brotli çalıştırmak yavaş; yalnızca
|
|
101
|
+
// tek seferlik build'de üretilir.
|
|
102
|
+
if (!watch) {
|
|
103
|
+
const { precompressAssets } = await import("./tasks/precompress.mjs");
|
|
104
|
+
await task("Precompress", precompressAssets);
|
|
105
|
+
log.section("output");
|
|
106
|
+
for (const row of manifestRows()) log.line(row);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (child) {
|
|
110
|
+
// Dev script'i "Ready" özetini build bitmeden basmasın diye işaret satırı.
|
|
111
|
+
process.stdout.write("[jskelet:build-ready]\n");
|
|
112
|
+
} else {
|
|
113
|
+
log.ready({
|
|
114
|
+
elapsed: Date.now() - started,
|
|
115
|
+
watching: watch,
|
|
116
|
+
label: watch ? "Ready" : "Built",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Manifest'teki her varlığın ham ve brotli boyutu.
|
|
122
|
+
* @returns {string[]}
|
|
123
|
+
*/
|
|
124
|
+
function manifestRows() {
|
|
125
|
+
return Object.entries(manifest)
|
|
126
|
+
.map(([name, url]) => {
|
|
127
|
+
const file = path.join(paths.public, url.replace(/^\//, ""));
|
|
128
|
+
const bytes = fs.existsSync(file) ? fs.statSync(file).size : 0;
|
|
129
|
+
const brotli = fs.existsSync(`${file}.br`) ? fs.statSync(`${file}.br`).size : 0;
|
|
130
|
+
return { name, bytes, brotli };
|
|
131
|
+
})
|
|
132
|
+
.sort((a, b) => b.bytes - a.bytes)
|
|
133
|
+
.map(
|
|
134
|
+
(item) =>
|
|
135
|
+
`${item.name.padEnd(22)}${log.size(item.bytes).padStart(9)}` +
|
|
136
|
+
`${item.brotli ? `${log.size(item.brotli).padStart(11)} br` : ""}`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `jskelet start` öncesi çalışır: build çıktısı yoksa üretir.
|
|
3
|
+
*
|
|
4
|
+
* Docker imajında build zaten yapıldığı için bu bir no-op; amaç `npm start`ı
|
|
5
|
+
* doğrudan çalıştıran birinin stilsiz bir sayfayla karşılaşmaması.
|
|
6
|
+
*/
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { loadConfig } from "../config/index.js";
|
|
10
|
+
|
|
11
|
+
const config = await loadConfig();
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(path.join(config.dirs.generated, "manifest.json"))) {
|
|
14
|
+
await import("./build.mjs");
|
|
15
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build çıktısı yolları ve hash'li varlık yazımı.
|
|
3
|
+
*
|
|
4
|
+
* Yollar `jskelet.config.mjs`'den gelir; `initBuildPaths()` çağrılmadan
|
|
5
|
+
* kullanılamazlar. Tek bir mutasyon noktası olması bilinçli: framework
|
|
6
|
+
* `node_modules/` içine girdiğinde `../..` sayarak kök bulmaya çalışan her
|
|
7
|
+
* dosya bozulur.
|
|
8
|
+
*/
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import crypto from "node:crypto";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @type {{ root: string, generated: string, public: string, assets: string,
|
|
15
|
+
* fonts: string, client: string, views: string, routes: string, styles: string }}
|
|
16
|
+
*/
|
|
17
|
+
export const paths = {
|
|
18
|
+
root: "",
|
|
19
|
+
generated: "",
|
|
20
|
+
public: "",
|
|
21
|
+
assets: "",
|
|
22
|
+
fonts: "",
|
|
23
|
+
client: "",
|
|
24
|
+
views: "",
|
|
25
|
+
routes: "",
|
|
26
|
+
styles: "",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {import('../config/index.js').ResolvedConfig} config
|
|
31
|
+
* @returns {void}
|
|
32
|
+
*/
|
|
33
|
+
export function initBuildPaths(config) {
|
|
34
|
+
Object.assign(paths, config.dirs, { root: config.root });
|
|
35
|
+
|
|
36
|
+
fs.mkdirSync(paths.generated, { recursive: true });
|
|
37
|
+
fs.mkdirSync(paths.assets, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* İçeriğe göre kısa hash. 10 hex karakter çakışma için fazlasıyla yeterli ve
|
|
42
|
+
* dosya adlarını okunur tutuyor.
|
|
43
|
+
*
|
|
44
|
+
* @param {string | Buffer} content
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
export function hash(content) {
|
|
48
|
+
return crypto.createHash("sha256").update(content).digest("hex").slice(0, 10);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Hash'li dosyayı `public/assets` altına yazar ve public yolunu döner.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} name Örn. "app.css"
|
|
55
|
+
* @param {string | Buffer} content
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
export function writeAsset(name, content) {
|
|
59
|
+
const ext = path.extname(name);
|
|
60
|
+
const base = name.slice(0, name.length - ext.length);
|
|
61
|
+
const fileName = `${base}.${hash(content)}${ext}`;
|
|
62
|
+
|
|
63
|
+
fs.mkdirSync(paths.assets, { recursive: true });
|
|
64
|
+
fs.writeFileSync(path.join(paths.assets, fileName), content);
|
|
65
|
+
|
|
66
|
+
return `/assets/${fileName}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** @returns {string} */
|
|
70
|
+
function manifestFile() {
|
|
71
|
+
return path.join(paths.generated, "manifest.json");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** @param {Record<string, string>} manifest */
|
|
75
|
+
export function writeManifest(manifest) {
|
|
76
|
+
fs.mkdirSync(paths.generated, { recursive: true });
|
|
77
|
+
fs.writeFileSync(manifestFile(), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Tek bir girdiyi günceller, diğerlerini korur.
|
|
82
|
+
*
|
|
83
|
+
* Watch modunda yeniden derlenen varlık yeni bir hash'e yazılıp eskisi
|
|
84
|
+
* silindiği için manifest de güncellenmek zorunda: aksi hâlde HTML silinmiş
|
|
85
|
+
* dosyayı isteyip 404 alır ve sayfa dev oturumunun kalanında stilsiz/JS'siz
|
|
86
|
+
* kalır.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} key
|
|
89
|
+
* @param {string} url
|
|
90
|
+
*/
|
|
91
|
+
export function patchManifest(key, url) {
|
|
92
|
+
/** @type {Record<string, string>} */
|
|
93
|
+
let current = {};
|
|
94
|
+
try {
|
|
95
|
+
current = JSON.parse(fs.readFileSync(manifestFile(), "utf8"));
|
|
96
|
+
} catch {
|
|
97
|
+
// Manifest henüz yoksa ilk yazımda oluşur.
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (current[key] === url) return;
|
|
101
|
+
|
|
102
|
+
current[key] = url;
|
|
103
|
+
writeManifest(current);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Eski hash'li çıktıları temizler.
|
|
108
|
+
*
|
|
109
|
+
* @param {string[]} prefixes
|
|
110
|
+
*/
|
|
111
|
+
export function pruneAssets(prefixes) {
|
|
112
|
+
if (!fs.existsSync(paths.assets)) return;
|
|
113
|
+
for (const file of fs.readdirSync(paths.assets)) {
|
|
114
|
+
if (prefixes.some((prefix) => file.startsWith(prefix))) {
|
|
115
|
+
fs.rmSync(path.join(paths.assets, file), { force: true });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opsiyonel peer bağımlılıklarını **uygulamanın** node_modules'ünden yükler.
|
|
3
|
+
*
|
|
4
|
+
* Framework `file:` ya da workspace bağlantısıyla kuruluysa kaynak dosyaları
|
|
5
|
+
* kendi dizininde çalışır ve düz bir `import "postcss"` framework'ün
|
|
6
|
+
* node_modules'üne bakar — uygulamanınkine değil. Tailwind/PostCSS/sharp gibi
|
|
7
|
+
* paketler uygulamanın devDependency'si olduğu için çözümlemeyi uygulama
|
|
8
|
+
* kökünden başlatmak zorundayız.
|
|
9
|
+
*/
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { createRequire } from "node:module";
|
|
12
|
+
import { pathToFileURL } from "node:url";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} root Uygulama kökü.
|
|
16
|
+
* @param {string} specifier
|
|
17
|
+
* @returns {Promise<any>}
|
|
18
|
+
* @throws Paket bulunamazsa; zorunlu bağımlılıklar için.
|
|
19
|
+
*/
|
|
20
|
+
export async function importFromApp(root, specifier) {
|
|
21
|
+
const require = createRequire(path.join(root, "package.json"));
|
|
22
|
+
return import(pathToFileURL(require.resolve(specifier)).href);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} root
|
|
27
|
+
* @param {string} specifier
|
|
28
|
+
* @returns {Promise<any | null>} Paket yoksa `null`; adım atlanabilsin diye.
|
|
29
|
+
*/
|
|
30
|
+
export async function tryImportFromApp(root, specifier) {
|
|
31
|
+
try {
|
|
32
|
+
return await importFromApp(root, specifier);
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|