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
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/** Island'ların paylaştığı küçük DOM yardımcıları. */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {ParentNode} root
|
|
5
|
+
* @param {string} selector
|
|
6
|
+
* @returns {HTMLElement | null}
|
|
7
|
+
*/
|
|
8
|
+
export function qs(root, selector) {
|
|
9
|
+
return /** @type {HTMLElement | null} */ (root.querySelector(selector));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {ParentNode} root
|
|
14
|
+
* @param {string} selector
|
|
15
|
+
* @returns {HTMLElement[]}
|
|
16
|
+
*/
|
|
17
|
+
export function qsa(root, selector) {
|
|
18
|
+
return /** @type {HTMLElement[]} */ ([...root.querySelectorAll(selector)]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Otomatik temizlenebilir listener.
|
|
23
|
+
* @param {EventTarget} target
|
|
24
|
+
* @param {string} type
|
|
25
|
+
* @param {EventListenerOrEventListenerObject} handler
|
|
26
|
+
* @param {AddEventListenerOptions | boolean} [options]
|
|
27
|
+
* @returns {() => void}
|
|
28
|
+
*/
|
|
29
|
+
export function on(target, type, handler, options) {
|
|
30
|
+
target.addEventListener(type, handler, options);
|
|
31
|
+
return () => target.removeEventListener(type, handler, options);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* `data-*` üzerinden delege edilmiş click.
|
|
36
|
+
* @param {HTMLElement} root
|
|
37
|
+
* @param {string} selector
|
|
38
|
+
* @param {(event: MouseEvent, target: HTMLElement) => void} handler
|
|
39
|
+
* @returns {() => void}
|
|
40
|
+
*/
|
|
41
|
+
export function onClick(root, selector, handler) {
|
|
42
|
+
return on(root, "click", (event) => {
|
|
43
|
+
const target = /** @type {HTMLElement} */ (event.target)?.closest(selector);
|
|
44
|
+
if (!target || !root.contains(target)) return;
|
|
45
|
+
handler(/** @type {MouseEvent} */ (event), /** @type {HTMLElement} */ (target));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {number} ms
|
|
51
|
+
* @param {(...args: any[]) => void} fn
|
|
52
|
+
* @returns {(...args: any[]) => void}
|
|
53
|
+
*/
|
|
54
|
+
export function debounce(ms, fn) {
|
|
55
|
+
let timer = null;
|
|
56
|
+
return (...args) => {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
timer = setTimeout(() => fn(...args), ms);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* requestAnimationFrame ile birleştirilmiş çağrı.
|
|
64
|
+
* @param {() => void} fn
|
|
65
|
+
* @returns {() => void}
|
|
66
|
+
*/
|
|
67
|
+
export function raf(fn) {
|
|
68
|
+
let frame = 0;
|
|
69
|
+
return () => {
|
|
70
|
+
if (frame) return;
|
|
71
|
+
frame = requestAnimationFrame(() => {
|
|
72
|
+
frame = 0;
|
|
73
|
+
fn();
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {HTMLElement} element
|
|
80
|
+
* @param {string} name
|
|
81
|
+
* @param {boolean} active
|
|
82
|
+
*/
|
|
83
|
+
export function toggleClass(element, name, active) {
|
|
84
|
+
element.classList.toggle(name, active);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Overlay portal hedefi: layout'ta `<div id="jskelet-overlays">` varsa
|
|
89
|
+
* modal/drawer içeriği oraya taşınır, yoksa `body`ye. Portal, `overflow`
|
|
90
|
+
* ya da `transform` taşıyan bir ata elementin `position: fixed` overlay'i
|
|
91
|
+
* kırpmasını engeller.
|
|
92
|
+
*/
|
|
93
|
+
export function getOverlayRoot() {
|
|
94
|
+
return document.getElementById("jskelet-overlays") ?? document.body;
|
|
95
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSkelet island runtime'ının tarayıcı tarafı API'si.
|
|
3
|
+
*
|
|
4
|
+
* `client/entries/main.js` içinde:
|
|
5
|
+
*
|
|
6
|
+
* import { registerAll, start } from "jskelet/client";
|
|
7
|
+
*
|
|
8
|
+
* registerAll({
|
|
9
|
+
* counter: () => import("../islands/counter.js"),
|
|
10
|
+
* });
|
|
11
|
+
*
|
|
12
|
+
* start();
|
|
13
|
+
*/
|
|
14
|
+
export { register, registerAll, hydrate, observeDocument, start } from "./registry.js";
|
|
15
|
+
export { createStore } from "./store.js";
|
|
16
|
+
export {
|
|
17
|
+
debounce,
|
|
18
|
+
on,
|
|
19
|
+
onClick,
|
|
20
|
+
qs,
|
|
21
|
+
qsa,
|
|
22
|
+
raf,
|
|
23
|
+
toggleClass,
|
|
24
|
+
getOverlayRoot,
|
|
25
|
+
} from "./dom.js";
|
|
26
|
+
export { startSafeImages } from "./safe-image.js";
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Island registry.
|
|
3
|
+
*
|
|
4
|
+
* Sunucu HTML'i tamdır; JS yalnızca davranış ekler. Bir element
|
|
5
|
+
* `data-island="ad"` taşıdığında ilgili modül dinamik import edilir ve
|
|
6
|
+
* `mount(element, props)` çağrılır.
|
|
7
|
+
*
|
|
8
|
+
* Hidrasyon **varsayılan olarak görünürlüğe bağlıdır**: her island bir
|
|
9
|
+
* IntersectionObserver'a verilir, ekranda olanlar zaten ilk gözlemde tetiklenir,
|
|
10
|
+
* ekran dışındakiler kaydırılana kadar hiç indirilmez. Ana sayfadaki grafik
|
|
11
|
+
* kütüphanesi gibi ağır modüller böylece ilk yükten tamamen çıkar.
|
|
12
|
+
*
|
|
13
|
+
* `data-island-eager` görünürlükten bağımsız, hemen bağlanmayı zorlar
|
|
14
|
+
* (header, çerez bandı gibi global davranışlar için).
|
|
15
|
+
*
|
|
16
|
+
* `data-island-idle` görünür olsa bile sayfa sakinleşene (load + boş zaman)
|
|
17
|
+
* kadar bekletir. İlk ekranda görünen ama kritik olmayan ağır modüller
|
|
18
|
+
* (ör. grafik kütüphanesi çeken mini grafik) LCP ile yarışmasın diye.
|
|
19
|
+
*
|
|
20
|
+
* `data-island-props` JSON ise parse edilip props olarak geçer.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** @typedef {(element: HTMLElement, props: object) => void | (() => void)} MountFn */
|
|
24
|
+
|
|
25
|
+
/** @type {Map<string, () => Promise<{ mount: MountFn }>>} */
|
|
26
|
+
const loaders = new Map();
|
|
27
|
+
|
|
28
|
+
/** @type {WeakMap<HTMLElement, Set<string>>} */
|
|
29
|
+
const mounted = new WeakMap();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} name
|
|
33
|
+
* @param {() => Promise<{ mount: MountFn }>} loader
|
|
34
|
+
*/
|
|
35
|
+
export function register(name, loader) {
|
|
36
|
+
loaders.set(name, loader);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** @param {Record<string, () => Promise<{ mount: MountFn }>>} entries */
|
|
40
|
+
export function registerAll(entries) {
|
|
41
|
+
for (const [name, loader] of Object.entries(entries)) {
|
|
42
|
+
register(name, loader);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {HTMLElement} element
|
|
48
|
+
* @returns {object}
|
|
49
|
+
*/
|
|
50
|
+
function readProps(element) {
|
|
51
|
+
const raw = element.dataset.islandProps;
|
|
52
|
+
if (!raw) return {};
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(raw);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error(
|
|
58
|
+
`[island] ${element.dataset.island}: geçersiz data-island-props`,
|
|
59
|
+
error,
|
|
60
|
+
);
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {HTMLElement} element
|
|
67
|
+
* @returns {boolean}
|
|
68
|
+
*/
|
|
69
|
+
function isMounted(element) {
|
|
70
|
+
const name = element.dataset.island;
|
|
71
|
+
if (!name) return true;
|
|
72
|
+
return mounted.get(element)?.has(name) ?? false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** @param {HTMLElement} element */
|
|
76
|
+
function markMounted(element) {
|
|
77
|
+
const name = element.dataset.island;
|
|
78
|
+
if (!name) return;
|
|
79
|
+
|
|
80
|
+
let names = mounted.get(element);
|
|
81
|
+
if (!names) {
|
|
82
|
+
names = new Set();
|
|
83
|
+
mounted.set(element, names);
|
|
84
|
+
}
|
|
85
|
+
names.add(name);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** @param {HTMLElement} element */
|
|
89
|
+
async function mountIsland(element) {
|
|
90
|
+
const name = element.dataset.island;
|
|
91
|
+
if (!name || isMounted(element)) return;
|
|
92
|
+
|
|
93
|
+
const loader = loaders.get(name);
|
|
94
|
+
if (!loader) {
|
|
95
|
+
console.warn(`[island] kayıtlı değil: ${name}`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
markMounted(element);
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const module = await loader();
|
|
103
|
+
module.mount(element, readProps(element));
|
|
104
|
+
element.dataset.islandReady = "true";
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error(`[island] ${name} yüklenemedi`, error);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** @type {IntersectionObserver | null} */
|
|
111
|
+
let lazyObserver = null;
|
|
112
|
+
|
|
113
|
+
function getLazyObserver() {
|
|
114
|
+
if (lazyObserver) return lazyObserver;
|
|
115
|
+
|
|
116
|
+
lazyObserver = new IntersectionObserver(
|
|
117
|
+
(entries) => {
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
if (!entry.isIntersecting) continue;
|
|
120
|
+
lazyObserver.unobserve(entry.target);
|
|
121
|
+
mountWhenReady(/** @type {HTMLElement} */ (entry.target));
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
{ rootMargin: "200px 0px" },
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
return lazyObserver;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Bağlama işini boş zamana kaydır: aynı anda görünen çok sayıda island'ın
|
|
132
|
+
* tek bir uzun task'a dönüşmesini engeller (TBT/INP).
|
|
133
|
+
*
|
|
134
|
+
* @param {() => void} fn
|
|
135
|
+
*/
|
|
136
|
+
function schedule(fn) {
|
|
137
|
+
if (typeof requestIdleCallback === "function") {
|
|
138
|
+
requestIdleCallback(fn, { timeout: 500 });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
setTimeout(fn, 0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* `load` tamamlanıp ana iş parçacığı boşalana kadar bekletir.
|
|
146
|
+
*
|
|
147
|
+
* @param {() => void} fn
|
|
148
|
+
*/
|
|
149
|
+
function afterLoad(fn) {
|
|
150
|
+
if (document.readyState === "complete") {
|
|
151
|
+
schedule(fn);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
window.addEventListener("load", () => schedule(fn), { once: true });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {HTMLElement} node
|
|
159
|
+
*/
|
|
160
|
+
function mountWhenReady(node) {
|
|
161
|
+
if ("islandIdle" in node.dataset) {
|
|
162
|
+
afterLoad(() => mountIsland(node));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
schedule(() => mountIsland(node));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** @param {ParentNode} [root] */
|
|
169
|
+
export function hydrate(root = document) {
|
|
170
|
+
/** @type {HTMLElement[]} */
|
|
171
|
+
const pending = [];
|
|
172
|
+
|
|
173
|
+
for (const element of root.querySelectorAll("[data-island]")) {
|
|
174
|
+
const node = /** @type {HTMLElement} */ (element);
|
|
175
|
+
if (!isMounted(node)) pending.push(node);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!pending.length) return;
|
|
179
|
+
|
|
180
|
+
// Ölçümler tek seferde okunur; araya yazma girmediği için tek reflow olur.
|
|
181
|
+
const boxed = pending.map((node) => node.getClientRects().length > 0);
|
|
182
|
+
|
|
183
|
+
pending.forEach((node, i) => {
|
|
184
|
+
// `hidden` bir drawer/dialog'un düzen kutusu yoktur ve IntersectionObserver
|
|
185
|
+
// onu asla bildirmez; bu yüzden gizli olanlar doğrudan bağlanır.
|
|
186
|
+
if ("islandEager" in node.dataset || !boxed[i]) {
|
|
187
|
+
mountIsland(node);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
getLazyObserver().observe(node);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Sonradan DOM'a eklenen island'ları da yakalar (infinite scroll, portal). */
|
|
196
|
+
export function observeDocument() {
|
|
197
|
+
const observer = new MutationObserver((mutations) => {
|
|
198
|
+
for (const mutation of mutations) {
|
|
199
|
+
for (const node of mutation.addedNodes) {
|
|
200
|
+
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
|
201
|
+
const element = /** @type {HTMLElement} */ (node);
|
|
202
|
+
if (element.dataset?.island) hydrate(element.parentNode ?? document);
|
|
203
|
+
if (element.querySelector?.("[data-island]")) hydrate(element);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
209
|
+
return observer;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function start() {
|
|
213
|
+
const run = () => {
|
|
214
|
+
hydrate();
|
|
215
|
+
observeDocument();
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
if (document.readyState === "loading") {
|
|
219
|
+
document.addEventListener("DOMContentLoaded", run, { once: true });
|
|
220
|
+
} else {
|
|
221
|
+
run();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yüklenemeyen görseller için tek bir belge dinleyicisi.
|
|
3
|
+
*
|
|
4
|
+
* Bu iş bilinçli olarak **island değil**. Görsel ağırlıklı bir sayfada 80+
|
|
5
|
+
* `<img>` olabiliyor ve her birine ayrı island bağlamak (gözlemci + dinamik
|
|
6
|
+
* import + mount) sırf hata ihtimali için ciddi bir hidrasyon yükü. `error`
|
|
7
|
+
* olayı kabarmaz ama yakalama fazında görülebilir, bu yüzden tek dinleyici
|
|
8
|
+
* hepsini karşılar ve sonradan DOM'a eklenen görseller de kendiliğinden
|
|
9
|
+
* kapsanır.
|
|
10
|
+
*
|
|
11
|
+
* Kullanım: görsele `data-safe-image` ekleyin. Kendi hata görünümünüzü
|
|
12
|
+
* vermek için görseli `data-safe-image-host` taşıyan bir sarmalayıcıya alın
|
|
13
|
+
* ve içine `<template data-safe-image-fallback>` koyun — framework hiçbir
|
|
14
|
+
* stil dayatmaz, yalnızca değiştirme işini yapar.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const MARKER = "data-safe-image";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Şablon verilmemişse yerine geçen minimal blok: görselin ölçülerini korur
|
|
21
|
+
* ki değiştirme sırasında düzen kaymasın (CLS).
|
|
22
|
+
*
|
|
23
|
+
* @param {HTMLImageElement} img
|
|
24
|
+
* @returns {HTMLElement}
|
|
25
|
+
*/
|
|
26
|
+
function buildFallback(img) {
|
|
27
|
+
const element = document.createElement("div");
|
|
28
|
+
const label = img.dataset.fallbackLabel || img.getAttribute("alt") || "";
|
|
29
|
+
|
|
30
|
+
element.className = [img.className, img.dataset.fallbackClass ?? ""]
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.join(" ");
|
|
33
|
+
element.dataset.safeImageFallback = "";
|
|
34
|
+
|
|
35
|
+
if (img.width && img.height) {
|
|
36
|
+
element.style.width = `${img.width}px`;
|
|
37
|
+
element.style.height = `${img.height}px`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
element.setAttribute("role", "img");
|
|
41
|
+
if (label) element.setAttribute("aria-label", label);
|
|
42
|
+
|
|
43
|
+
return element;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {HTMLImageElement} img
|
|
48
|
+
*/
|
|
49
|
+
function replace(img) {
|
|
50
|
+
if (!img.isConnected) return;
|
|
51
|
+
|
|
52
|
+
const host = img.closest("[data-safe-image-host]");
|
|
53
|
+
const template = host?.querySelector("template[data-safe-image-fallback]");
|
|
54
|
+
|
|
55
|
+
if (host && template) {
|
|
56
|
+
host.replaceWith(template.content.cloneNode(true));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
img.replaceWith(buildFallback(img));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @returns {void}
|
|
65
|
+
*/
|
|
66
|
+
export function startSafeImages() {
|
|
67
|
+
document.addEventListener(
|
|
68
|
+
"error",
|
|
69
|
+
(event) => {
|
|
70
|
+
const target = /** @type {HTMLElement} */ (event.target);
|
|
71
|
+
if (target?.tagName !== "IMG" || !target.hasAttribute(MARKER)) return;
|
|
72
|
+
replace(/** @type {HTMLImageElement} */ (target));
|
|
73
|
+
},
|
|
74
|
+
// `error` kabarmaz; yalnızca yakalama fazında görülür.
|
|
75
|
+
true,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// JS çalışmadan önce başarısız olmuş görseller olay üretmez; bir kez taranır.
|
|
79
|
+
const sweep = () => {
|
|
80
|
+
for (const img of document.querySelectorAll(`img[${MARKER}]`)) {
|
|
81
|
+
const image = /** @type {HTMLImageElement} */ (img);
|
|
82
|
+
if (image.complete && image.naturalWidth === 0) replace(image);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (typeof requestIdleCallback === "function") {
|
|
87
|
+
requestIdleCallback(sweep, { timeout: 2000 });
|
|
88
|
+
} else {
|
|
89
|
+
setTimeout(sweep, 0);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Context yerine kullanılan minimal pub/sub store.
|
|
3
|
+
* `useSyncExternalStore` köprüsünün yerini alır: doğrudan `subscribe`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {T} initial
|
|
8
|
+
* @returns {{
|
|
9
|
+
* get: () => T,
|
|
10
|
+
* set: (next: T | ((prev: T) => T)) => void,
|
|
11
|
+
* subscribe: (listener: (value: T) => void) => () => void,
|
|
12
|
+
* }}
|
|
13
|
+
* @template T
|
|
14
|
+
*/
|
|
15
|
+
export function createStore(initial) {
|
|
16
|
+
let value = initial;
|
|
17
|
+
/** @type {Set<(value: T) => void>} */
|
|
18
|
+
const listeners = new Set();
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
get: () => value,
|
|
22
|
+
|
|
23
|
+
set(next) {
|
|
24
|
+
const resolved =
|
|
25
|
+
typeof next === "function" ? /** @type {Function} */ (next)(value) : next;
|
|
26
|
+
if (resolved === value) return;
|
|
27
|
+
value = resolved;
|
|
28
|
+
for (const listener of listeners) listener(value);
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
subscribe(listener) {
|
|
32
|
+
listeners.add(listener);
|
|
33
|
+
return () => listeners.delete(listener);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework varsayılanları.
|
|
3
|
+
*
|
|
4
|
+
* Buradaki hiçbir değer domain bilgisi taşımaz: uygulamaya özel her şey
|
|
5
|
+
* `jskelet.config.mjs` üzerinden gelir. Varsayılanların ayrı dosyada olması,
|
|
6
|
+
* "framework ne yapıyor, uygulama ne ekliyor" ayrımını okunur tutar.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Uzun süre cache'lenecek hash'li çıktı dizini. */
|
|
10
|
+
export const IMMUTABLE_CACHE = "public, max-age=31536000, immutable";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Proje kökündeki dizin adları. `jskelet.config.mjs` içinde `paths` ile
|
|
14
|
+
* tek tek ezilebilir.
|
|
15
|
+
*/
|
|
16
|
+
export const DEFAULT_DIRS = {
|
|
17
|
+
/** EJS layout + sayfalar + bileşenler. */
|
|
18
|
+
views: "views",
|
|
19
|
+
/** Statik dosyalar; build çıktısı da buraya yazılır. */
|
|
20
|
+
public: "public",
|
|
21
|
+
/** Island runtime ve entry'ler. */
|
|
22
|
+
client: "client",
|
|
23
|
+
/** Route modülleri. */
|
|
24
|
+
routes: "routes",
|
|
25
|
+
/** Tailwind/PostCSS giriş dosyası. */
|
|
26
|
+
styles: "styles/globals.css",
|
|
27
|
+
/** Build ara çıktıları (manifest, metafile, images). */
|
|
28
|
+
generated: ".jskelet",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Uzantı ve önek bazlı statik dosya tespiti. Bu listeye uyan yollara
|
|
33
|
+
* `IMMUTABLE_CACHE` yazılır.
|
|
34
|
+
*/
|
|
35
|
+
export const DEFAULT_STATIC = {
|
|
36
|
+
extensions: [".svg", ".png", ".webp", ".avif", ".ico", ".woff2"],
|
|
37
|
+
prefixes: ["/assets/", "/fonts/"],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Dev gate'in hiçbir koşulda kapatmadığı yollar. */
|
|
41
|
+
export const DEFAULT_DEV_GATE_BYPASS = [
|
|
42
|
+
"/api/healthcheck",
|
|
43
|
+
"/robots.txt",
|
|
44
|
+
"/sitemap.xml",
|
|
45
|
+
"/site.webmanifest",
|
|
46
|
+
"/favicon.ico",
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
/** Prewarm ayarlarının kod içi varsayılanları. */
|
|
50
|
+
export const DEFAULT_PREWARM = {
|
|
51
|
+
enabled: true,
|
|
52
|
+
max: 400,
|
|
53
|
+
intervalSeconds: 0,
|
|
54
|
+
/** Paralel ısıtma isteği; dev'de sunucu tek süreç olduğu için düşürülür. */
|
|
55
|
+
concurrency: 4,
|
|
56
|
+
/** İki tur arasında beklenen süre: upstream'e ani yük binmesin. */
|
|
57
|
+
delayMs: 0,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** Oturuma bağlı sayfalar ısıtılmaz; uygulama kendi listesini verebilir. */
|
|
61
|
+
export const DEFAULT_PREWARM_SKIP = ["/api/", "/_fragment/", "/__jskelet/"];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Site içi gezinme ipuçları (Speculation Rules + view transition).
|
|
65
|
+
*
|
|
66
|
+
* Varsayılan bilinçli olarak ölçülü: `prefetch` açık, çünkü yalnızca belgeyi
|
|
67
|
+
* indirir ve sayfanın JS'ini çalıştırmaz — yanlış tahmin edilse bile tek
|
|
68
|
+
* maliyeti bir istektir. `prerender` kapalı, çünkü hedef sayfanın script'leri
|
|
69
|
+
* gerçekten çalışır; ölçüm kodunu `prerenderingchange` olayına bağlamayan bir
|
|
70
|
+
* uygulamada ziyaret sayıları şişer. `viewTransition` kapalı, çünkü uygulamanın
|
|
71
|
+
* kendi geçiş animasyonlarıyla çakışabilir.
|
|
72
|
+
*/
|
|
73
|
+
export const DEFAULT_NAVIGATION = {
|
|
74
|
+
/** `false` ya da eagerness: "conservative" | "moderate" | "eager". */
|
|
75
|
+
prefetch: /** @type {false | "conservative" | "moderate" | "eager"} */ ("moderate"),
|
|
76
|
+
/** @type {false | "conservative" | "moderate" | "eager"} */
|
|
77
|
+
prerender: false,
|
|
78
|
+
/** `@view-transition { navigation: auto }` basılsın mı. */
|
|
79
|
+
viewTransition: false,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Hiçbir koşulda önden getirilmeyecek yollar. Yan etkisi olan ya da gezinme
|
|
84
|
+
* hedefi olmayan uçların spekülatif istekle tetiklenmesi gerçek bir hata
|
|
85
|
+
* kaynağı; uygulama kendi listesini `navigation.exclude` ile ekler.
|
|
86
|
+
*/
|
|
87
|
+
export const DEFAULT_NAVIGATION_EXCLUDE = ["/api/*", "/_fragment/*"];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Markalama. Header adı ve dev overlay yolu tek yerden değişsin diye
|
|
91
|
+
* config'ten okunur — fork eden proje kendi adını verebilir.
|
|
92
|
+
*/
|
|
93
|
+
export const DEFAULT_BRAND = {
|
|
94
|
+
name: "JSkelet",
|
|
95
|
+
/** `<html lang>`; uygulama kendi dilini config'te bildirir. */
|
|
96
|
+
lang: "en",
|
|
97
|
+
poweredBy: "JSkelet",
|
|
98
|
+
cacheHeader: "X-JSkelet-Cache",
|
|
99
|
+
devBasePath: "/__jskelet/dev",
|
|
100
|
+
prewarmUserAgent: "jskelet-prewarm",
|
|
101
|
+
devTokenCookie: "dev_token",
|
|
102
|
+
};
|