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.
Files changed (72) hide show
  1. package/AGENTS.md +127 -0
  2. package/CHANGELOG.md +40 -0
  3. package/LICENSE +21 -0
  4. package/README.md +342 -0
  5. package/bin/jskelet.mjs +104 -0
  6. package/docs/01-baslangic.md +285 -0
  7. package/docs/02-mimari.md +287 -0
  8. package/docs/03-routing.md +437 -0
  9. package/docs/04-render-ve-sablonlar.md +490 -0
  10. package/docs/05-islands.md +429 -0
  11. package/docs/06-cache.md +409 -0
  12. package/docs/07-yapilandirma.md +673 -0
  13. package/docs/08-build.md +366 -0
  14. package/docs/09-dev-araclari.md +302 -0
  15. package/docs/10-dagitim.md +329 -0
  16. package/docs/11-tasima.md +352 -0
  17. package/docs/README.md +82 -0
  18. package/package.json +97 -0
  19. package/src/build/build.mjs +138 -0
  20. package/src/build/ensure-build.mjs +15 -0
  21. package/src/build/paths.mjs +118 -0
  22. package/src/build/resolve-peer.mjs +36 -0
  23. package/src/build/tasks/client.mjs +268 -0
  24. package/src/build/tasks/css.mjs +124 -0
  25. package/src/build/tasks/fonts.mjs +146 -0
  26. package/src/build/tasks/icons.mjs +224 -0
  27. package/src/build/tasks/images.mjs +244 -0
  28. package/src/build/tasks/precompress.mjs +78 -0
  29. package/src/client/devtools/overlay.js +1763 -0
  30. package/src/client/devtools/report.html +185 -0
  31. package/src/client/devtools/report.js +712 -0
  32. package/src/client/dom.js +95 -0
  33. package/src/client/index.js +26 -0
  34. package/src/client/registry.js +223 -0
  35. package/src/client/safe-image.js +91 -0
  36. package/src/client/store.js +36 -0
  37. package/src/config/defaults.js +102 -0
  38. package/src/config/index.js +433 -0
  39. package/src/config/pattern.js +107 -0
  40. package/src/dev-server.mjs +383 -0
  41. package/src/http/control-flow.js +56 -0
  42. package/src/http/request-cache.js +46 -0
  43. package/src/index.js +35 -0
  44. package/src/init.mjs +220 -0
  45. package/src/log.mjs +332 -0
  46. package/src/logo.png +0 -0
  47. package/src/runtime/alias-hooks.mjs +119 -0
  48. package/src/runtime/register.mjs +4 -0
  49. package/src/server/assets.js +119 -0
  50. package/src/server/create-app.js +167 -0
  51. package/src/server/dev/devtools.js +383 -0
  52. package/src/server/dev/report.js +351 -0
  53. package/src/server/head-hints.js +132 -0
  54. package/src/server/html-cache.js +166 -0
  55. package/src/server/metadata.js +102 -0
  56. package/src/server/middleware/compression.js +205 -0
  57. package/src/server/middleware/dev-gate.js +62 -0
  58. package/src/server/middleware/headers.js +37 -0
  59. package/src/server/middleware/redirects.js +32 -0
  60. package/src/server/middleware/static-precompressed.js +100 -0
  61. package/src/server/middleware/upstream-proxy.js +141 -0
  62. package/src/server/prewarm.js +283 -0
  63. package/src/server/render.js +356 -0
  64. package/src/server/router.js +121 -0
  65. package/src/server/status-page.js +164 -0
  66. package/src/server/upstream-tracking.js +51 -0
  67. package/src/start.mjs +7 -0
  68. package/src/templates/layout.ejs +44 -0
  69. package/src/version.mjs +17 -0
  70. package/src/views/components/loader.js +85 -0
  71. package/src/views/helpers/html.js +102 -0
  72. 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
+ }