jskelet 0.6.1 → 0.6.3

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 (155) hide show
  1. package/AGENTS.md +136 -136
  2. package/CHANGELOG.md +620 -596
  3. package/LICENSE +21 -21
  4. package/bin/jskelet.mjs +130 -130
  5. package/docs/01-baslangic.md +291 -291
  6. package/docs/02-mimari.md +310 -309
  7. package/docs/03-routing.md +515 -515
  8. package/docs/04-render-ve-sablonlar.md +661 -661
  9. package/docs/05-islands.md +486 -486
  10. package/docs/06-cache.md +1443 -1423
  11. package/docs/07-yapilandirma.md +12 -6
  12. package/docs/08-build.md +429 -428
  13. package/docs/09-dev-araclari.md +364 -364
  14. package/docs/10-dagitim.md +338 -338
  15. package/docs/12-panel-ve-oturum.md +478 -478
  16. package/docs/README.md +83 -83
  17. package/docs/en/01-getting-started.md +298 -298
  18. package/docs/en/02-architecture.md +329 -328
  19. package/docs/en/03-routing.md +531 -531
  20. package/docs/en/04-rendering.md +669 -669
  21. package/docs/en/05-islands.md +497 -497
  22. package/docs/en/06-caching.md +1453 -1431
  23. package/docs/en/07-configuration.md +1219 -1214
  24. package/docs/en/08-build.md +447 -446
  25. package/docs/en/09-dev-tools.md +373 -373
  26. package/docs/en/10-deployment.md +340 -340
  27. package/docs/en/11-migration.md +398 -398
  28. package/docs/en/12-dashboards-and-sessions.md +488 -488
  29. package/docs/en/README.md +87 -87
  30. package/package.json +137 -137
  31. package/src/build/ensure-build.mjs +19 -19
  32. package/src/build/paths.mjs +153 -153
  33. package/src/build/resolve-peer.mjs +36 -36
  34. package/src/build/tasks/client.mjs +349 -349
  35. package/src/build/tasks/css.mjs +235 -235
  36. package/src/build/tasks/fonts.mjs +146 -146
  37. package/src/build/tasks/icons.mjs +357 -357
  38. package/src/build/tasks/images.mjs +244 -244
  39. package/src/build/tasks/precompress.mjs +78 -78
  40. package/src/build/tasks/templates.mjs +20 -20
  41. package/src/client/admin/i18n.js +764 -764
  42. package/src/client/admin/login.html +74 -74
  43. package/src/client/admin/panel.css +809 -809
  44. package/src/client/admin/panel.html +495 -495
  45. package/src/client/admin/panel.js +1251 -1251
  46. package/src/client/devtools/report.html +185 -185
  47. package/src/client/devtools/report.js +745 -745
  48. package/src/client/devtools/seo.js +628 -628
  49. package/src/client/dom.js +95 -95
  50. package/src/client/form.js +192 -192
  51. package/src/client/index.js +45 -45
  52. package/src/client/registry.js +305 -305
  53. package/src/client/safe-image.js +91 -91
  54. package/src/client/shared-cookie.js +225 -225
  55. package/src/client/store.js +36 -36
  56. package/src/client/swap.js +188 -188
  57. package/src/compile/codegen.js +336 -336
  58. package/src/compile/compile-all.js +149 -149
  59. package/src/compile/errors.js +66 -66
  60. package/src/compile/expr.js +409 -409
  61. package/src/compile/index.js +17 -17
  62. package/src/compile/parse.js +541 -541
  63. package/src/compile/resolve.js +211 -211
  64. package/src/compile/scan-exports.js +51 -51
  65. package/src/config/defaults.js +17 -1
  66. package/src/config/index.js +13 -0
  67. package/src/config/pattern.js +107 -107
  68. package/src/generate.mjs +163 -163
  69. package/src/http/control-flow.js +71 -71
  70. package/src/http/cookies-entry.js +21 -21
  71. package/src/http/cookies.js +277 -277
  72. package/src/http/request-cache.js +46 -46
  73. package/src/http/request-context.js +165 -165
  74. package/src/http/shared-cookie.js +178 -178
  75. package/src/index.js +101 -101
  76. package/src/init.mjs +230 -230
  77. package/src/migrate/apply.mjs +262 -262
  78. package/src/migrate/babel.mjs +79 -79
  79. package/src/migrate/classify.mjs +155 -155
  80. package/src/migrate/config.mjs +126 -126
  81. package/src/migrate/fs-walk.mjs +191 -191
  82. package/src/migrate/parse.mjs +26 -26
  83. package/src/migrate/scan.mjs +177 -177
  84. package/src/migrate/transform/expr-source.mjs +168 -168
  85. package/src/migrate/transform/island.mjs +67 -67
  86. package/src/migrate/transform/jsx-to-component.mjs +302 -302
  87. package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
  88. package/src/migrate/transform/page-split.mjs +435 -435
  89. package/src/migrate/write.mjs +81 -81
  90. package/src/migrate.mjs +171 -171
  91. package/src/runtime/alias-hooks.mjs +119 -119
  92. package/src/runtime/register.mjs +4 -4
  93. package/src/server/admin/actions.js +229 -229
  94. package/src/server/admin/auth.js +125 -125
  95. package/src/server/admin/event-log.js +151 -151
  96. package/src/server/admin/gate.js +209 -209
  97. package/src/server/admin/inventory.js +188 -188
  98. package/src/server/admin/mount.js +56 -56
  99. package/src/server/admin/router.js +216 -216
  100. package/src/server/admin/snapshot.js +241 -241
  101. package/src/server/assets.js +147 -147
  102. package/src/server/auth/handoff.js +309 -309
  103. package/src/server/cache-blob.js +70 -0
  104. package/src/server/cache-deps.js +42 -42
  105. package/src/server/cache-vary.js +113 -113
  106. package/src/server/cloudflare.js +607 -607
  107. package/src/server/create-app.js +366 -366
  108. package/src/server/data-cache.js +553 -462
  109. package/src/server/dev/report.js +485 -485
  110. package/src/server/dev/socket.js +170 -170
  111. package/src/server/dev/version-check.mjs +139 -139
  112. package/src/server/disk-cache.js +233 -0
  113. package/src/server/ejs-adapter.js +59 -59
  114. package/src/server/html-cache.js +1196 -1122
  115. package/src/server/image-optimizer.js +500 -407
  116. package/src/server/logs/access-middleware.js +66 -66
  117. package/src/server/logs/file-sink.js +193 -66
  118. package/src/server/logs/pipeline.js +165 -158
  119. package/src/server/logs/s3-put.js +214 -214
  120. package/src/server/logs/s3-sink.js +112 -112
  121. package/src/server/metadata.js +102 -102
  122. package/src/server/middleware/compression.js +205 -205
  123. package/src/server/middleware/csrf.js +134 -134
  124. package/src/server/middleware/dev-gate.js +75 -75
  125. package/src/server/middleware/headers.js +37 -37
  126. package/src/server/middleware/redirects.js +32 -32
  127. package/src/server/middleware/robots-txt.js +341 -341
  128. package/src/server/middleware/static-precompressed.js +121 -100
  129. package/src/server/middleware/trailing-slash.js +53 -53
  130. package/src/server/middleware/upstream-proxy.js +141 -141
  131. package/src/server/og-image.js +356 -356
  132. package/src/server/port-guard.js +255 -255
  133. package/src/server/prewarm.js +1082 -1058
  134. package/src/server/redis.js +588 -569
  135. package/src/server/render.js +4 -4
  136. package/src/server/router.js +157 -157
  137. package/src/server/status-page.js +265 -265
  138. package/src/server/upstream-limiter.js +376 -376
  139. package/src/server/upstream-tracking.js +166 -166
  140. package/src/shared/cookie-domain.js +66 -66
  141. package/src/start.mjs +22 -22
  142. package/src/templates/layout.ejs +30 -30
  143. package/src/templates/layout.jsk +30 -30
  144. package/src/version.mjs +31 -31
  145. package/src/views/components/loader.js +101 -101
  146. package/src/views/helpers/html.js +102 -102
  147. package/src/views/helpers/tags.js +375 -375
  148. package/types/config/defaults.d.ts +15 -1
  149. package/types/config/index.d.ts +8 -0
  150. package/types/server/cache-blob.d.ts +13 -0
  151. package/types/server/data-cache.d.ts +9 -0
  152. package/types/server/disk-cache.d.ts +36 -0
  153. package/types/server/html-cache.d.ts +26 -3
  154. package/types/server/logs/file-sink.d.ts +16 -5
  155. package/types/server/redis.d.ts +2 -1
@@ -1,305 +1,305 @@
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
- * `mount()` bir temizlik fonksiyonu döndürebiliyor; DOM'dan çıkan island'ın
33
- * dinleyicilerini ve zamanlayıcılarını sökmek için saklanır.
34
- *
35
- * WeakMap: sökülmeden çöpe giden bir element (ör. tüm sayfa gezinmesi)
36
- * burada referans bırakmasın. Alt ağacı gezme işi DOM üzerinden yapılıyor,
37
- * bu yüzden ayrı bir element listesine gerek yok.
38
- *
39
- * @type {WeakMap<HTMLElement, (() => void)[]>}
40
- */
41
- const cleanups = new WeakMap();
42
-
43
- /**
44
- * @param {string} name
45
- * @param {() => Promise<{ mount: MountFn }>} loader
46
- */
47
- export function register(name, loader) {
48
- loaders.set(name, loader);
49
- }
50
-
51
- /** @param {Record<string, () => Promise<{ mount: MountFn }>>} entries */
52
- export function registerAll(entries) {
53
- for (const [name, loader] of Object.entries(entries)) {
54
- register(name, loader);
55
- }
56
- }
57
-
58
- /**
59
- * @param {HTMLElement} element
60
- * @returns {object}
61
- */
62
- function readProps(element) {
63
- const raw = element.dataset.islandProps;
64
- if (!raw) return {};
65
-
66
- try {
67
- return JSON.parse(raw);
68
- } catch (error) {
69
- console.error(
70
- `[island] ${element.dataset.island}: invalid data-island-props`,
71
- error,
72
- );
73
- return {};
74
- }
75
- }
76
-
77
- /**
78
- * @param {HTMLElement} element
79
- * @returns {boolean}
80
- */
81
- function isMounted(element) {
82
- const name = element.dataset.island;
83
- if (!name) return true;
84
- return mounted.get(element)?.has(name) ?? false;
85
- }
86
-
87
- /** @param {HTMLElement} element */
88
- function markMounted(element) {
89
- const name = element.dataset.island;
90
- if (!name) return;
91
-
92
- let names = mounted.get(element);
93
- if (!names) {
94
- names = new Set();
95
- mounted.set(element, names);
96
- }
97
- names.add(name);
98
- }
99
-
100
- /** @param {HTMLElement} element */
101
- async function mountIsland(element) {
102
- const name = element.dataset.island;
103
- if (!name || isMounted(element)) return;
104
-
105
- const loader = loaders.get(name);
106
- if (!loader) {
107
- console.warn(`[island] not registered: ${name}`);
108
- return;
109
- }
110
-
111
- markMounted(element);
112
-
113
- // Dev overlay başarısız fetch'i hangi island'dan geldiğine bağlayabilsin.
114
- // Mount bittikten sonra (event handler'lar) bu işaret kalkar; o durumda
115
- // yığın izine düşülür.
116
- const previous = globalThis.__JSKELET_MOUNTING_ISLAND;
117
- globalThis.__JSKELET_MOUNTING_ISLAND = name;
118
-
119
- try {
120
- const module = await loader();
121
- const cleanup = module.mount(element, readProps(element));
122
-
123
- if (typeof cleanup === "function") {
124
- // Island bağlanırken DOM'dan çıkarılmış olabilir (hızlı bir fragment
125
- // takası). Temizliği saklamak yerine hemen çalıştırmak gerekiyor;
126
- // aksi hâlde artık görünmeyen bir elementin dinleyicileri hiç sökülmez.
127
- if (element.isConnected) {
128
- const list = cleanups.get(element) ?? [];
129
- list.push(cleanup);
130
- cleanups.set(element, list);
131
- } else {
132
- runCleanups(element, cleanup);
133
- }
134
- }
135
-
136
- element.dataset.islandReady = "true";
137
- } catch (error) {
138
- console.error(`[island] ${name} failed to load`, error);
139
- } finally {
140
- globalThis.__JSKELET_MOUNTING_ISLAND = previous;
141
- }
142
- }
143
-
144
- /**
145
- * @param {HTMLElement} element
146
- * @param {() => void} [extra] Henüz saklanmamış temizlik.
147
- */
148
- function runCleanups(element, extra) {
149
- const stored = cleanups.get(element) ?? [];
150
- cleanups.delete(element);
151
- mounted.delete(element);
152
- delete element.dataset.islandReady;
153
-
154
- for (const cleanup of extra ? [...stored, extra] : stored) {
155
- try {
156
- cleanup();
157
- } catch (error) {
158
- console.error(`[island] ${element.dataset.island} cleanup failed`, error);
159
- }
160
- }
161
- }
162
-
163
- /**
164
- * Bir alt ağaçtaki island'ları söker.
165
- *
166
- * Fragment takasında çağrılması zorunlu: `innerHTML` ile değiştirilen bir
167
- * bölgenin island'ları DOM'dan çıkar ama `document`/`window` üzerine
168
- * kurdukları dinleyiciler ve `setInterval`'ları yaşamaya devam eder. Birkaç
169
- * takastan sonra aynı olay birden fazla kez işlenmeye başlar.
170
- *
171
- * Sökülen element yeniden bağlanabilir hâle gelir: `mounted` kaydı da
172
- * temizlenir, böylece aynı düğüm tekrar DOM'a girerse `hydrate()` onu
173
- * yeniden görür.
174
- *
175
- * @param {ParentNode} [root] Kökün kendisi de island olabilir.
176
- */
177
- export function unmount(root = document) {
178
- const element = /** @type {HTMLElement} */ (root);
179
-
180
- if (element.dataset?.island) {
181
- lazyObserver?.unobserve(element);
182
- runCleanups(element);
183
- }
184
-
185
- for (const node of root.querySelectorAll?.("[data-island]") ?? []) {
186
- const child = /** @type {HTMLElement} */ (node);
187
- lazyObserver?.unobserve(child);
188
- runCleanups(child);
189
- }
190
- }
191
-
192
- /** @type {IntersectionObserver | null} */
193
- let lazyObserver = null;
194
-
195
- function getLazyObserver() {
196
- if (lazyObserver) return lazyObserver;
197
-
198
- lazyObserver = new IntersectionObserver(
199
- (entries) => {
200
- for (const entry of entries) {
201
- if (!entry.isIntersecting) continue;
202
- lazyObserver.unobserve(entry.target);
203
- mountWhenReady(/** @type {HTMLElement} */ (entry.target));
204
- }
205
- },
206
- { rootMargin: "200px 0px" },
207
- );
208
-
209
- return lazyObserver;
210
- }
211
-
212
- /**
213
- * Bağlama işini boş zamana kaydır: aynı anda görünen çok sayıda island'ın
214
- * tek bir uzun task'a dönüşmesini engeller (TBT/INP).
215
- *
216
- * @param {() => void} fn
217
- */
218
- function schedule(fn) {
219
- if (typeof requestIdleCallback === "function") {
220
- requestIdleCallback(fn, { timeout: 500 });
221
- return;
222
- }
223
- setTimeout(fn, 0);
224
- }
225
-
226
- /**
227
- * `load` tamamlanıp ana iş parçacığı boşalana kadar bekletir.
228
- *
229
- * @param {() => void} fn
230
- */
231
- function afterLoad(fn) {
232
- if (document.readyState === "complete") {
233
- schedule(fn);
234
- return;
235
- }
236
- window.addEventListener("load", () => schedule(fn), { once: true });
237
- }
238
-
239
- /**
240
- * @param {HTMLElement} node
241
- */
242
- function mountWhenReady(node) {
243
- if ("islandIdle" in node.dataset) {
244
- afterLoad(() => mountIsland(node));
245
- return;
246
- }
247
- schedule(() => mountIsland(node));
248
- }
249
-
250
- /** @param {ParentNode} [root] */
251
- export function hydrate(root = document) {
252
- /** @type {HTMLElement[]} */
253
- const pending = [];
254
-
255
- for (const element of root.querySelectorAll("[data-island]")) {
256
- const node = /** @type {HTMLElement} */ (element);
257
- if (!isMounted(node)) pending.push(node);
258
- }
259
-
260
- if (!pending.length) return;
261
-
262
- // Ölçümler tek seferde okunur; araya yazma girmediği için tek reflow olur.
263
- const boxed = pending.map((node) => node.getClientRects().length > 0);
264
-
265
- pending.forEach((node, i) => {
266
- // `hidden` bir drawer/dialog'un düzen kutusu yoktur ve IntersectionObserver
267
- // onu asla bildirmez; bu yüzden gizli olanlar doğrudan bağlanır.
268
- if ("islandEager" in node.dataset || !boxed[i]) {
269
- mountIsland(node);
270
- return;
271
- }
272
-
273
- getLazyObserver().observe(node);
274
- });
275
- }
276
-
277
- /** Sonradan DOM'a eklenen island'ları da yakalar (infinite scroll, portal). */
278
- export function observeDocument() {
279
- const observer = new MutationObserver((mutations) => {
280
- for (const mutation of mutations) {
281
- for (const node of mutation.addedNodes) {
282
- if (node.nodeType !== Node.ELEMENT_NODE) continue;
283
- const element = /** @type {HTMLElement} */ (node);
284
- if (element.dataset?.island) hydrate(element.parentNode ?? document);
285
- if (element.querySelector?.("[data-island]")) hydrate(element);
286
- }
287
- }
288
- });
289
-
290
- observer.observe(document.body, { childList: true, subtree: true });
291
- return observer;
292
- }
293
-
294
- export function start() {
295
- const run = () => {
296
- hydrate();
297
- observeDocument();
298
- };
299
-
300
- if (document.readyState === "loading") {
301
- document.addEventListener("DOMContentLoaded", run, { once: true });
302
- } else {
303
- run();
304
- }
305
- }
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
+ * `mount()` bir temizlik fonksiyonu döndürebiliyor; DOM'dan çıkan island'ın
33
+ * dinleyicilerini ve zamanlayıcılarını sökmek için saklanır.
34
+ *
35
+ * WeakMap: sökülmeden çöpe giden bir element (ör. tüm sayfa gezinmesi)
36
+ * burada referans bırakmasın. Alt ağacı gezme işi DOM üzerinden yapılıyor,
37
+ * bu yüzden ayrı bir element listesine gerek yok.
38
+ *
39
+ * @type {WeakMap<HTMLElement, (() => void)[]>}
40
+ */
41
+ const cleanups = new WeakMap();
42
+
43
+ /**
44
+ * @param {string} name
45
+ * @param {() => Promise<{ mount: MountFn }>} loader
46
+ */
47
+ export function register(name, loader) {
48
+ loaders.set(name, loader);
49
+ }
50
+
51
+ /** @param {Record<string, () => Promise<{ mount: MountFn }>>} entries */
52
+ export function registerAll(entries) {
53
+ for (const [name, loader] of Object.entries(entries)) {
54
+ register(name, loader);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * @param {HTMLElement} element
60
+ * @returns {object}
61
+ */
62
+ function readProps(element) {
63
+ const raw = element.dataset.islandProps;
64
+ if (!raw) return {};
65
+
66
+ try {
67
+ return JSON.parse(raw);
68
+ } catch (error) {
69
+ console.error(
70
+ `[island] ${element.dataset.island}: invalid data-island-props`,
71
+ error,
72
+ );
73
+ return {};
74
+ }
75
+ }
76
+
77
+ /**
78
+ * @param {HTMLElement} element
79
+ * @returns {boolean}
80
+ */
81
+ function isMounted(element) {
82
+ const name = element.dataset.island;
83
+ if (!name) return true;
84
+ return mounted.get(element)?.has(name) ?? false;
85
+ }
86
+
87
+ /** @param {HTMLElement} element */
88
+ function markMounted(element) {
89
+ const name = element.dataset.island;
90
+ if (!name) return;
91
+
92
+ let names = mounted.get(element);
93
+ if (!names) {
94
+ names = new Set();
95
+ mounted.set(element, names);
96
+ }
97
+ names.add(name);
98
+ }
99
+
100
+ /** @param {HTMLElement} element */
101
+ async function mountIsland(element) {
102
+ const name = element.dataset.island;
103
+ if (!name || isMounted(element)) return;
104
+
105
+ const loader = loaders.get(name);
106
+ if (!loader) {
107
+ console.warn(`[island] not registered: ${name}`);
108
+ return;
109
+ }
110
+
111
+ markMounted(element);
112
+
113
+ // Dev overlay başarısız fetch'i hangi island'dan geldiğine bağlayabilsin.
114
+ // Mount bittikten sonra (event handler'lar) bu işaret kalkar; o durumda
115
+ // yığın izine düşülür.
116
+ const previous = globalThis.__JSKELET_MOUNTING_ISLAND;
117
+ globalThis.__JSKELET_MOUNTING_ISLAND = name;
118
+
119
+ try {
120
+ const module = await loader();
121
+ const cleanup = module.mount(element, readProps(element));
122
+
123
+ if (typeof cleanup === "function") {
124
+ // Island bağlanırken DOM'dan çıkarılmış olabilir (hızlı bir fragment
125
+ // takası). Temizliği saklamak yerine hemen çalıştırmak gerekiyor;
126
+ // aksi hâlde artık görünmeyen bir elementin dinleyicileri hiç sökülmez.
127
+ if (element.isConnected) {
128
+ const list = cleanups.get(element) ?? [];
129
+ list.push(cleanup);
130
+ cleanups.set(element, list);
131
+ } else {
132
+ runCleanups(element, cleanup);
133
+ }
134
+ }
135
+
136
+ element.dataset.islandReady = "true";
137
+ } catch (error) {
138
+ console.error(`[island] ${name} failed to load`, error);
139
+ } finally {
140
+ globalThis.__JSKELET_MOUNTING_ISLAND = previous;
141
+ }
142
+ }
143
+
144
+ /**
145
+ * @param {HTMLElement} element
146
+ * @param {() => void} [extra] Henüz saklanmamış temizlik.
147
+ */
148
+ function runCleanups(element, extra) {
149
+ const stored = cleanups.get(element) ?? [];
150
+ cleanups.delete(element);
151
+ mounted.delete(element);
152
+ delete element.dataset.islandReady;
153
+
154
+ for (const cleanup of extra ? [...stored, extra] : stored) {
155
+ try {
156
+ cleanup();
157
+ } catch (error) {
158
+ console.error(`[island] ${element.dataset.island} cleanup failed`, error);
159
+ }
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Bir alt ağaçtaki island'ları söker.
165
+ *
166
+ * Fragment takasında çağrılması zorunlu: `innerHTML` ile değiştirilen bir
167
+ * bölgenin island'ları DOM'dan çıkar ama `document`/`window` üzerine
168
+ * kurdukları dinleyiciler ve `setInterval`'ları yaşamaya devam eder. Birkaç
169
+ * takastan sonra aynı olay birden fazla kez işlenmeye başlar.
170
+ *
171
+ * Sökülen element yeniden bağlanabilir hâle gelir: `mounted` kaydı da
172
+ * temizlenir, böylece aynı düğüm tekrar DOM'a girerse `hydrate()` onu
173
+ * yeniden görür.
174
+ *
175
+ * @param {ParentNode} [root] Kökün kendisi de island olabilir.
176
+ */
177
+ export function unmount(root = document) {
178
+ const element = /** @type {HTMLElement} */ (root);
179
+
180
+ if (element.dataset?.island) {
181
+ lazyObserver?.unobserve(element);
182
+ runCleanups(element);
183
+ }
184
+
185
+ for (const node of root.querySelectorAll?.("[data-island]") ?? []) {
186
+ const child = /** @type {HTMLElement} */ (node);
187
+ lazyObserver?.unobserve(child);
188
+ runCleanups(child);
189
+ }
190
+ }
191
+
192
+ /** @type {IntersectionObserver | null} */
193
+ let lazyObserver = null;
194
+
195
+ function getLazyObserver() {
196
+ if (lazyObserver) return lazyObserver;
197
+
198
+ lazyObserver = new IntersectionObserver(
199
+ (entries) => {
200
+ for (const entry of entries) {
201
+ if (!entry.isIntersecting) continue;
202
+ lazyObserver.unobserve(entry.target);
203
+ mountWhenReady(/** @type {HTMLElement} */ (entry.target));
204
+ }
205
+ },
206
+ { rootMargin: "200px 0px" },
207
+ );
208
+
209
+ return lazyObserver;
210
+ }
211
+
212
+ /**
213
+ * Bağlama işini boş zamana kaydır: aynı anda görünen çok sayıda island'ın
214
+ * tek bir uzun task'a dönüşmesini engeller (TBT/INP).
215
+ *
216
+ * @param {() => void} fn
217
+ */
218
+ function schedule(fn) {
219
+ if (typeof requestIdleCallback === "function") {
220
+ requestIdleCallback(fn, { timeout: 500 });
221
+ return;
222
+ }
223
+ setTimeout(fn, 0);
224
+ }
225
+
226
+ /**
227
+ * `load` tamamlanıp ana iş parçacığı boşalana kadar bekletir.
228
+ *
229
+ * @param {() => void} fn
230
+ */
231
+ function afterLoad(fn) {
232
+ if (document.readyState === "complete") {
233
+ schedule(fn);
234
+ return;
235
+ }
236
+ window.addEventListener("load", () => schedule(fn), { once: true });
237
+ }
238
+
239
+ /**
240
+ * @param {HTMLElement} node
241
+ */
242
+ function mountWhenReady(node) {
243
+ if ("islandIdle" in node.dataset) {
244
+ afterLoad(() => mountIsland(node));
245
+ return;
246
+ }
247
+ schedule(() => mountIsland(node));
248
+ }
249
+
250
+ /** @param {ParentNode} [root] */
251
+ export function hydrate(root = document) {
252
+ /** @type {HTMLElement[]} */
253
+ const pending = [];
254
+
255
+ for (const element of root.querySelectorAll("[data-island]")) {
256
+ const node = /** @type {HTMLElement} */ (element);
257
+ if (!isMounted(node)) pending.push(node);
258
+ }
259
+
260
+ if (!pending.length) return;
261
+
262
+ // Ölçümler tek seferde okunur; araya yazma girmediği için tek reflow olur.
263
+ const boxed = pending.map((node) => node.getClientRects().length > 0);
264
+
265
+ pending.forEach((node, i) => {
266
+ // `hidden` bir drawer/dialog'un düzen kutusu yoktur ve IntersectionObserver
267
+ // onu asla bildirmez; bu yüzden gizli olanlar doğrudan bağlanır.
268
+ if ("islandEager" in node.dataset || !boxed[i]) {
269
+ mountIsland(node);
270
+ return;
271
+ }
272
+
273
+ getLazyObserver().observe(node);
274
+ });
275
+ }
276
+
277
+ /** Sonradan DOM'a eklenen island'ları da yakalar (infinite scroll, portal). */
278
+ export function observeDocument() {
279
+ const observer = new MutationObserver((mutations) => {
280
+ for (const mutation of mutations) {
281
+ for (const node of mutation.addedNodes) {
282
+ if (node.nodeType !== Node.ELEMENT_NODE) continue;
283
+ const element = /** @type {HTMLElement} */ (node);
284
+ if (element.dataset?.island) hydrate(element.parentNode ?? document);
285
+ if (element.querySelector?.("[data-island]")) hydrate(element);
286
+ }
287
+ }
288
+ });
289
+
290
+ observer.observe(document.body, { childList: true, subtree: true });
291
+ return observer;
292
+ }
293
+
294
+ export function start() {
295
+ const run = () => {
296
+ hydrate();
297
+ observeDocument();
298
+ };
299
+
300
+ if (document.readyState === "loading") {
301
+ document.addEventListener("DOMContentLoaded", run, { once: true });
302
+ } else {
303
+ run();
304
+ }
305
+ }