sibujs 1.5.0 → 2.1.0

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 (208) hide show
  1. package/dist/browser.cjs +332 -121
  2. package/dist/browser.d.cts +5 -0
  3. package/dist/browser.d.ts +5 -0
  4. package/dist/browser.js +6 -6
  5. package/dist/build.cjs +1049 -344
  6. package/dist/build.js +15 -13
  7. package/dist/cdn.global.js +17 -16
  8. package/dist/chunk-2RA7SHDA.js +65 -0
  9. package/dist/chunk-2UPRY23K.js +80 -0
  10. package/dist/{chunk-BMPL52BF.js → chunk-3DZP6OIT.js} +118 -66
  11. package/dist/chunk-3JHCYHWN.js +125 -0
  12. package/dist/{chunk-CZUGLNJS.js → chunk-45YP72ZQ.js} +3 -3
  13. package/dist/{chunk-JCDUJN2F.js → chunk-AMK2TYNW.js} +490 -153
  14. package/dist/{chunk-NHUC2QWH.js → chunk-CWBVQML6.js} +1 -1
  15. package/dist/{chunk-XHK6BDAJ.js → chunk-DRUZZAK4.js} +25 -8
  16. package/dist/{chunk-RJ46C3CS.js → chunk-GWWURC5M.js} +71 -20
  17. package/dist/{chunk-3X2YG6YM.js → chunk-JYD2PWXH.js} +59 -28
  18. package/dist/{chunk-2BYQDGN3.js → chunk-KGYT6UO6.js} +234 -63
  19. package/dist/{chunk-5X6PP2UK.js → chunk-LMLD24FC.js} +2 -2
  20. package/dist/{chunk-M4NLBH4I.js → chunk-LYTCUZ7H.js} +3 -2
  21. package/dist/{chunk-XUEEGU5O.js → chunk-NASX6ST2.js} +16 -4
  22. package/dist/{chunk-VQDZK23A.js → chunk-O6EFQ3KT.js} +181 -66
  23. package/dist/{chunk-BGN5ZMP4.js → chunk-OJ3P4ECI.js} +14 -2
  24. package/dist/chunk-ON5MMR2J.js +1327 -0
  25. package/dist/{chunk-SFKNRVCU.js → chunk-P2HSJDDN.js} +135 -79
  26. package/dist/chunk-QO3WC6FS.js +384 -0
  27. package/dist/{chunk-WZSPOOER.js → chunk-RDTDJCAB.js} +8 -5
  28. package/dist/{chunk-7GRNSCFT.js → chunk-TH2ILCYW.js} +312 -185
  29. package/dist/chunk-UCS6AMJ7.js +79 -0
  30. package/dist/{chunk-VAPYJN4X.js → chunk-V6C4FADE.js} +93 -23
  31. package/dist/{chunk-OUZZEE4S.js → chunk-WANSMF2L.js} +17 -11
  32. package/dist/{chunk-23VV7YD3.js → chunk-WIPZPFBQ.js} +25 -30
  33. package/dist/chunk-WZA53FXU.js +149 -0
  34. package/dist/{chunk-BGTHZHJ5.js → chunk-ZAQSMOED.js} +188 -44
  35. package/dist/{customElement-BL3Uo8dL.d.cts → customElement-CPfIrbvg.d.cts} +14 -10
  36. package/dist/{customElement-BL3Uo8dL.d.ts → customElement-CPfIrbvg.d.ts} +14 -10
  37. package/dist/data.cjs +536 -151
  38. package/dist/data.d.cts +20 -2
  39. package/dist/data.d.ts +20 -2
  40. package/dist/data.js +11 -9
  41. package/dist/devtools.cjs +613 -266
  42. package/dist/devtools.d.cts +1 -1
  43. package/dist/devtools.d.ts +1 -1
  44. package/dist/devtools.js +12 -6
  45. package/dist/ecosystem.cjs +602 -197
  46. package/dist/ecosystem.d.cts +9 -7
  47. package/dist/ecosystem.d.ts +9 -7
  48. package/dist/ecosystem.js +12 -11
  49. package/dist/extras.cjs +3500 -1608
  50. package/dist/extras.d.cts +9 -9
  51. package/dist/extras.d.ts +9 -9
  52. package/dist/extras.js +58 -45
  53. package/dist/index.cjs +1055 -344
  54. package/dist/index.d.cts +85 -8
  55. package/dist/index.d.ts +85 -8
  56. package/dist/index.js +32 -16
  57. package/dist/{introspect-BumjnBKr.d.cts → introspect-2TOlQ7oa.d.cts} +25 -3
  58. package/dist/{introspect-CZrlcaYy.d.ts → introspect-DnIpHQQz.d.ts} +25 -3
  59. package/dist/motion.cjs +122 -63
  60. package/dist/motion.js +4 -4
  61. package/dist/patterns.cjs +450 -110
  62. package/dist/patterns.d.cts +11 -12
  63. package/dist/patterns.d.ts +11 -12
  64. package/dist/patterns.js +7 -7
  65. package/dist/performance.cjs +373 -149
  66. package/dist/performance.d.cts +23 -16
  67. package/dist/performance.d.ts +23 -16
  68. package/dist/performance.js +13 -8
  69. package/dist/plugin-D30wlGW5.d.cts +71 -0
  70. package/dist/plugin-D30wlGW5.d.ts +71 -0
  71. package/dist/plugins.cjs +729 -301
  72. package/dist/plugins.d.cts +10 -3
  73. package/dist/plugins.d.ts +10 -3
  74. package/dist/plugins.js +106 -38
  75. package/dist/{ssr-Do_SiVoL.d.cts → ssr-CrVNy6Pa.d.cts} +9 -15
  76. package/dist/{ssr-Do_SiVoL.d.ts → ssr-CrVNy6Pa.d.ts} +9 -15
  77. package/dist/{ssr-4PBXAOO3.js → ssr-FXD2PPMC.js} +4 -3
  78. package/dist/ssr.cjs +736 -274
  79. package/dist/ssr.d.cts +26 -6
  80. package/dist/ssr.d.ts +26 -6
  81. package/dist/ssr.js +12 -11
  82. package/dist/{tagFactory-DaJ0YWX6.d.cts → tagFactory-S17H2qxu.d.cts} +9 -1
  83. package/dist/{tagFactory-DaJ0YWX6.d.ts → tagFactory-S17H2qxu.d.ts} +9 -1
  84. package/dist/testing.cjs +303 -76
  85. package/dist/testing.d.cts +17 -4
  86. package/dist/testing.d.ts +17 -4
  87. package/dist/testing.js +100 -44
  88. package/dist/ui.cjs +589 -178
  89. package/dist/ui.d.cts +1 -1
  90. package/dist/ui.d.ts +1 -1
  91. package/dist/ui.js +20 -17
  92. package/dist/widgets.cjs +1103 -146
  93. package/dist/widgets.d.cts +104 -2
  94. package/dist/widgets.d.ts +104 -2
  95. package/dist/widgets.js +9 -7
  96. package/package.json +8 -2
  97. package/dist/chunk-32DY64NT.js +0 -282
  98. package/dist/chunk-3AIRKM3B.js +0 -1263
  99. package/dist/chunk-3ARAQO7B.js +0 -398
  100. package/dist/chunk-3CRQALYP.js +0 -877
  101. package/dist/chunk-4EI4AG32.js +0 -482
  102. package/dist/chunk-4MYMUBRS.js +0 -21
  103. package/dist/chunk-5ZYQ6KDD.js +0 -154
  104. package/dist/chunk-6BMPXPUW.js +0 -26
  105. package/dist/chunk-6HLLIF3K.js +0 -398
  106. package/dist/chunk-6LSNVCS2.js +0 -937
  107. package/dist/chunk-6SA3QQES.js +0 -61
  108. package/dist/chunk-77L6NL3X.js +0 -1097
  109. package/dist/chunk-7BF6TK55.js +0 -1097
  110. package/dist/chunk-7TQKR4PP.js +0 -294
  111. package/dist/chunk-7V26P53V.js +0 -712
  112. package/dist/chunk-AZ3ISID5.js +0 -298
  113. package/dist/chunk-B7SWRFUT.js +0 -332
  114. package/dist/chunk-BTU3TJDS.js +0 -365
  115. package/dist/chunk-BW3WT46K.js +0 -937
  116. package/dist/chunk-C6KFWOFV.js +0 -616
  117. package/dist/chunk-CHF5OHIA.js +0 -61
  118. package/dist/chunk-CHJ27IGK.js +0 -26
  119. package/dist/chunk-CMBFNA7L.js +0 -27
  120. package/dist/chunk-DAHRH4ON.js +0 -331
  121. package/dist/chunk-DKOHBI74.js +0 -924
  122. package/dist/chunk-DTCOOBMX.js +0 -725
  123. package/dist/chunk-EBGIRKQY.js +0 -616
  124. package/dist/chunk-EUZND3CB.js +0 -27
  125. package/dist/chunk-EVCZO745.js +0 -365
  126. package/dist/chunk-EWFVA3TJ.js +0 -282
  127. package/dist/chunk-F3FA4F32.js +0 -292
  128. package/dist/chunk-FGOEVHY3.js +0 -60
  129. package/dist/chunk-G3BOQPVO.js +0 -365
  130. package/dist/chunk-GCOK2LC3.js +0 -282
  131. package/dist/chunk-GJPXRJ45.js +0 -37
  132. package/dist/chunk-HGMJFBC7.js +0 -654
  133. package/dist/chunk-JAKHTMQU.js +0 -1000
  134. package/dist/chunk-JCI5M6U6.js +0 -956
  135. package/dist/chunk-K4G4ZQNR.js +0 -286
  136. package/dist/chunk-K5ZUMYVS.js +0 -89
  137. package/dist/chunk-KQPDEVVS.js +0 -398
  138. package/dist/chunk-L6JRBDNS.js +0 -60
  139. package/dist/chunk-LA6KQEDU.js +0 -712
  140. package/dist/chunk-MB6QFH3I.js +0 -2776
  141. package/dist/chunk-MDVXJWFN.js +0 -304
  142. package/dist/chunk-MEZVEBPN.js +0 -2008
  143. package/dist/chunk-MK4ERFYL.js +0 -2249
  144. package/dist/chunk-MLKGABMK.js +0 -9
  145. package/dist/chunk-MQ5GOYPH.js +0 -2249
  146. package/dist/chunk-MYRV7VDM.js +0 -742
  147. package/dist/chunk-N6IZB6KJ.js +0 -567
  148. package/dist/chunk-NEKUBFPT.js +0 -60
  149. package/dist/chunk-NMRUZALC.js +0 -1097
  150. package/dist/chunk-NYVAC6P5.js +0 -37
  151. package/dist/chunk-NZIIMDWI.js +0 -84
  152. package/dist/chunk-OF7UZIVB.js +0 -725
  153. package/dist/chunk-P3XWXJZU.js +0 -282
  154. package/dist/chunk-P6W3STU4.js +0 -2249
  155. package/dist/chunk-PBHF5WKN.js +0 -616
  156. package/dist/chunk-PDZQY43A.js +0 -616
  157. package/dist/chunk-PTQJDMRT.js +0 -146
  158. package/dist/chunk-PZEGYCF5.js +0 -61
  159. package/dist/chunk-QBMDLBU2.js +0 -975
  160. package/dist/chunk-QWZG56ET.js +0 -2744
  161. package/dist/chunk-RQGQSLQK.js +0 -725
  162. package/dist/chunk-SDLZDHKP.js +0 -107
  163. package/dist/chunk-TDGZL5CU.js +0 -365
  164. package/dist/chunk-TNQWPPE6.js +0 -37
  165. package/dist/chunk-TSOKIX5Z.js +0 -654
  166. package/dist/chunk-UHNL42EF.js +0 -2730
  167. package/dist/chunk-UNXCEF6S.js +0 -21
  168. package/dist/chunk-V2XTI523.js +0 -347
  169. package/dist/chunk-VAU366PN.js +0 -2241
  170. package/dist/chunk-VMVDTCXB.js +0 -712
  171. package/dist/chunk-VQNQZCWJ.js +0 -61
  172. package/dist/chunk-VRW3FULF.js +0 -725
  173. package/dist/chunk-WADYRCO2.js +0 -304
  174. package/dist/chunk-WILQZRO4.js +0 -282
  175. package/dist/chunk-WR5D4EGH.js +0 -26
  176. package/dist/chunk-WUHJISPP.js +0 -298
  177. package/dist/chunk-XYU6TZOW.js +0 -182
  178. package/dist/chunk-Y6GP4QGG.js +0 -276
  179. package/dist/chunk-YECR7UIA.js +0 -347
  180. package/dist/chunk-YUTWTI4B.js +0 -654
  181. package/dist/chunk-Z65KYU7I.js +0 -26
  182. package/dist/chunk-Z6POF5YC.js +0 -975
  183. package/dist/chunk-ZBJP6WFL.js +0 -482
  184. package/dist/chunk-ZD6OAMTH.js +0 -277
  185. package/dist/chunk-ZWKZCBO6.js +0 -317
  186. package/dist/contracts-DDrwxvJ-.d.cts +0 -245
  187. package/dist/contracts-DDrwxvJ-.d.ts +0 -245
  188. package/dist/contracts-DOrhwbke.d.cts +0 -245
  189. package/dist/contracts-DOrhwbke.d.ts +0 -245
  190. package/dist/contracts-xo5ckdRP.d.cts +0 -240
  191. package/dist/contracts-xo5ckdRP.d.ts +0 -240
  192. package/dist/customElement-BKQfbSZQ.d.cts +0 -262
  193. package/dist/customElement-BKQfbSZQ.d.ts +0 -262
  194. package/dist/customElement-D2DJp_xn.d.cts +0 -313
  195. package/dist/customElement-D2DJp_xn.d.ts +0 -313
  196. package/dist/customElement-yz8uyk-0.d.cts +0 -308
  197. package/dist/customElement-yz8uyk-0.d.ts +0 -308
  198. package/dist/introspect-Cb0zgpi2.d.cts +0 -477
  199. package/dist/introspect-Y2xNXGSf.d.ts +0 -477
  200. package/dist/plugin-Bek4RhJY.d.cts +0 -43
  201. package/dist/plugin-Bek4RhJY.d.ts +0 -43
  202. package/dist/ssr-3RXHP5ES.js +0 -38
  203. package/dist/ssr-6GIMY5MX.js +0 -38
  204. package/dist/ssr-BA6sxxUd.d.cts +0 -135
  205. package/dist/ssr-BA6sxxUd.d.ts +0 -135
  206. package/dist/ssr-WKUPVSSK.js +0 -36
  207. package/dist/tagFactory-Dl8QCLga.d.cts +0 -23
  208. package/dist/tagFactory-Dl8QCLga.d.ts +0 -23
package/dist/plugins.cjs CHANGED
@@ -26,12 +26,12 @@ function isDev() {
26
26
  }
27
27
  function devAssert(condition, message) {
28
28
  if (_isDev && !condition) {
29
- throw new Error(`[Sibu] ${message}`);
29
+ throw new Error(`[SibuJS] ${message}`);
30
30
  }
31
31
  }
32
32
  function devWarn(message) {
33
33
  if (_isDev) {
34
- console.warn(`[Sibu] ${message}`);
34
+ console.warn(`[SibuJS] ${message}`);
35
35
  }
36
36
  }
37
37
  var _isDev;
@@ -47,14 +47,47 @@ function sanitizeUrl(url) {
47
47
  const trimmed = url.replace(/[\x00-\x20\x7f-\x9f]+/g, "").trim();
48
48
  if (!trimmed) return "";
49
49
  const lower = trimmed.toLowerCase();
50
- if (lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("vbscript:") || lower.startsWith("blob:")) {
51
- return "";
50
+ let schemeEnd = -1;
51
+ for (let i2 = 0; i2 < lower.length; i2++) {
52
+ const ch = lower.charCodeAt(i2);
53
+ if (ch === 58) {
54
+ schemeEnd = i2;
55
+ break;
56
+ }
57
+ if (ch === 47 || ch === 63 || ch === 35) break;
52
58
  }
59
+ if (schemeEnd === -1) return trimmed;
60
+ const scheme = lower.slice(0, schemeEnd + 1);
61
+ if (!/^[a-z][a-z0-9+.-]*:$/.test(scheme)) return trimmed;
62
+ if (SAFE_URL_PROTOCOLS.indexOf(scheme) === -1) return "";
53
63
  return trimmed;
54
64
  }
65
+ function sanitizeSrcset(value) {
66
+ const parts = value.split(",");
67
+ const out = [];
68
+ for (let i2 = 0; i2 < parts.length; i2++) {
69
+ const part = parts[i2].trim();
70
+ if (!part) continue;
71
+ const m = part.match(/^(\S+)(\s+.+)?$/);
72
+ if (!m) continue;
73
+ const safe = sanitizeUrl(m[1]);
74
+ if (!safe) continue;
75
+ out.push(m[2] ? `${safe}${m[2]}` : safe);
76
+ }
77
+ return out.join(", ");
78
+ }
55
79
  function sanitizeCSSValue(value) {
56
- const lower = value.toLowerCase().replace(/\s+/g, "");
57
- if (lower.includes("url(") || lower.includes("expression(") || lower.includes("javascript:") || lower.includes("-moz-binding")) {
80
+ const decoded = value.replace(/\\([0-9a-fA-F]{1,6})\s?/g, (_m, hex) => {
81
+ const code2 = Number.parseInt(hex, 16);
82
+ if (!Number.isFinite(code2) || code2 < 0 || code2 > 1114111) return "";
83
+ try {
84
+ return String.fromCodePoint(code2);
85
+ } catch {
86
+ return "";
87
+ }
88
+ });
89
+ const lower = decoded.toLowerCase().replace(/\s+/g, "");
90
+ if (lower.includes("url(") || lower.includes("expression(") || lower.includes("javascript:") || lower.includes("vbscript:") || lower.includes("-moz-binding") || lower.includes("behavior:") || lower.includes("@import") || lower.includes("image-set(") || lower.includes("filter:progid")) {
58
91
  return "";
59
92
  }
60
93
  return value;
@@ -62,11 +95,56 @@ function sanitizeCSSValue(value) {
62
95
  function isUrlAttribute(attr) {
63
96
  return URL_ATTRIBUTES.has(attr);
64
97
  }
65
- var URL_ATTRIBUTES;
98
+ var SAFE_URL_PROTOCOLS, URL_ATTRIBUTES;
66
99
  var init_sanitize = __esm({
67
100
  "src/utils/sanitize.ts"() {
68
101
  "use strict";
69
- URL_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "cite", "poster", "background", "srcset"]);
102
+ SAFE_URL_PROTOCOLS = ["http:", "https:", "mailto:", "tel:", "ftp:"];
103
+ URL_ATTRIBUTES = /* @__PURE__ */ new Set([
104
+ "href",
105
+ "xlink:href",
106
+ "src",
107
+ "action",
108
+ "formaction",
109
+ "formtarget",
110
+ "cite",
111
+ "poster",
112
+ "background",
113
+ "srcset",
114
+ "ping",
115
+ "data"
116
+ ]);
117
+ }
118
+ });
119
+
120
+ // src/core/ssr-context.ts
121
+ function getSSRStore() {
122
+ if (als) {
123
+ const s2 = als.getStore();
124
+ if (s2) return s2;
125
+ }
126
+ return fallbackStore;
127
+ }
128
+ function isSSR() {
129
+ return getSSRStore().ssr;
130
+ }
131
+ var als, fallbackStore;
132
+ var init_ssr_context = __esm({
133
+ "src/core/ssr-context.ts"() {
134
+ "use strict";
135
+ als = null;
136
+ try {
137
+ if (typeof process !== "undefined" && process.versions && process.versions.node) {
138
+ const req = Function("return typeof require==='function'?require:null")();
139
+ if (req) {
140
+ const mod = req("node:async_hooks");
141
+ als = new mod.AsyncLocalStorage();
142
+ }
143
+ }
144
+ } catch {
145
+ als = null;
146
+ }
147
+ fallbackStore = { ssr: false, suspenseIdCounter: 0 };
70
148
  }
71
149
  });
72
150
 
@@ -100,7 +178,7 @@ function isEventHandlerAttr2(name) {
100
178
  return lower[0] === "o" && lower[1] === "n" && lower.charCodeAt(2) >= 97 && lower.charCodeAt(2) <= 122;
101
179
  }
102
180
  function ssrErrorComment(err) {
103
- if (_isDev7) {
181
+ if (_isDev8) {
104
182
  const msg = escapeHtml(err instanceof Error ? err.message : String(err));
105
183
  return `<!--SSR error: ${safeCommentText(msg)}-->`;
106
184
  }
@@ -130,10 +208,10 @@ function renderToString(element) {
130
208
  }
131
209
  const tag = element.tagName.toLowerCase();
132
210
  if (tag === "script" || tag === "style") {
133
- return _isDev7 ? `<!--ssr:${tag}-stripped-->` : "";
211
+ return _isDev8 ? `<!--ssr:${tag}-stripped-->` : "";
134
212
  }
135
213
  if (!/^[a-z][a-z0-9-]*$/i.test(tag)) {
136
- return _isDev7 ? "<!--ssr:invalid-tag-->" : "";
214
+ return _isDev8 ? "<!--ssr:invalid-tag-->" : "";
137
215
  }
138
216
  let html2 = `<${tag}`;
139
217
  for (const attr of Array.from(element.attributes)) {
@@ -174,9 +252,9 @@ function hydrate(component, container, options = {}) {
174
252
  const first = mismatches[0];
175
253
  if (options.onMismatch) {
176
254
  options.onMismatch(first);
177
- } else if (_isDev7) {
255
+ } else if (_isDev8) {
178
256
  console.warn(
179
- `[Sibu hydration] ${first.message}
257
+ `[SibuJS hydration] ${first.message}
180
258
  at ${first.path}
181
259
  server: ${first.serverValue}
182
260
  client: ${first.clientValue}`
@@ -184,17 +262,9 @@ function hydrate(component, container, options = {}) {
184
262
  }
185
263
  }
186
264
  }
187
- hydrateNode(container.firstElementChild, clientTree);
265
+ container.replaceChildren(clientTree);
188
266
  container.setAttribute("data-sibu-hydrated", "true");
189
267
  }
190
- function hydrateNode(serverNode, clientNode) {
191
- if (!serverNode) return;
192
- const serverChildren = Array.from(serverNode.children);
193
- const clientChildren = Array.from(clientNode.children);
194
- for (let i2 = 0; i2 < Math.min(serverChildren.length, clientChildren.length); i2++) {
195
- hydrateNode(serverChildren[i2], clientChildren[i2]);
196
- }
197
- }
198
268
  function collectMismatches(serverNode, clientNode, path2, out, max = 5) {
199
269
  if (out.length >= max) return;
200
270
  const nodePath = path2 || clientNode?.tagName?.toLowerCase() || "(root)";
@@ -372,11 +442,11 @@ async function* renderToStream(element) {
372
442
  }
373
443
  const tag = element.tagName.toLowerCase();
374
444
  if (tag === "script" || tag === "style") {
375
- if (_isDev7) yield `<!--ssr:${tag}-stripped-->`;
445
+ if (_isDev8) yield `<!--ssr:${tag}-stripped-->`;
376
446
  return;
377
447
  }
378
448
  if (!/^[a-z][a-z0-9-]*$/i.test(tag)) {
379
- if (_isDev7) yield "<!--ssr:invalid-tag-->";
449
+ if (_isDev8) yield "<!--ssr:invalid-tag-->";
380
450
  return;
381
451
  }
382
452
  let openTag = `<${tag}`;
@@ -424,12 +494,15 @@ function renderToReadableStream(element) {
424
494
  controller.enqueue(value);
425
495
  }
426
496
  },
427
- cancel() {
428
- generator.return(void 0);
497
+ async cancel() {
498
+ await generator.return(void 0);
429
499
  }
430
500
  });
431
501
  }
432
502
  function island(id, component) {
503
+ if (!SAFE_ID.test(id)) {
504
+ throw new Error(`[SibuJS SSR] island: id must match [A-Za-z0-9_-]+ (got: ${JSON.stringify(id.slice(0, 32))})`);
505
+ }
433
506
  const el = component();
434
507
  el.setAttribute("data-sibu-island", id);
435
508
  return el;
@@ -442,8 +515,9 @@ function hydrateIslands(container, islands) {
442
515
  const factory = islands[id];
443
516
  if (typeof factory !== "function") continue;
444
517
  const clientTree = factory();
445
- hydrateNode(marker2, clientTree);
446
- marker2.setAttribute("data-sibu-hydrated", "true");
518
+ clientTree.setAttribute("data-sibu-island", id);
519
+ clientTree.setAttribute("data-sibu-hydrated", "true");
520
+ marker2.replaceWith(clientTree);
447
521
  }
448
522
  container.setAttribute("data-sibu-hydrated", "partial");
449
523
  }
@@ -460,8 +534,9 @@ function hydrateProgressively(container, islands, options) {
460
534
  for (const entry of entries) {
461
535
  if (entry.isIntersecting) {
462
536
  const clientTree = factory();
463
- hydrateNode(marker2, clientTree);
464
- marker2.setAttribute("data-sibu-hydrated", "true");
537
+ clientTree.setAttribute("data-sibu-island", id);
538
+ clientTree.setAttribute("data-sibu-hydrated", "true");
539
+ marker2.replaceWith(clientTree);
465
540
  observer.disconnect();
466
541
  break;
467
542
  }
@@ -478,18 +553,36 @@ function hydrateProgressively(container, islands, options) {
478
553
  };
479
554
  }
480
555
  function resetSSRState() {
481
- suspenseIdCounter = 0;
556
+ getSSRStore().suspenseIdCounter = 0;
557
+ }
558
+ function noop() {
482
559
  }
483
560
  function ssrSuspense(props) {
484
- const id = `sibu-sus-${suspenseIdCounter++}`;
561
+ const store = getSSRStore();
562
+ const id = `sibu-sus-${store.suspenseIdCounter++}`;
563
+ const timeoutMs = props.timeoutMs ?? 3e4;
485
564
  const fallbackEl = props.fallback();
486
565
  const wrapper = document.createElement("div");
487
566
  wrapper.setAttribute("data-sibu-suspense-id", id);
488
567
  wrapper.appendChild(fallbackEl);
489
- const promise = props.content().then((resolvedEl) => ({
490
- id,
491
- html: renderToString(resolvedEl)
492
- }));
568
+ const fallbackHtml = renderToString(fallbackEl);
569
+ let timer;
570
+ const timeoutPromise = new Promise((_, reject) => {
571
+ timer = setTimeout(() => reject(new Error(`[SibuJS SSR] ssrSuspense timed out after ${timeoutMs}ms`)), timeoutMs);
572
+ });
573
+ const raced = Promise.race([props.content(), timeoutPromise]);
574
+ const promise = raced.then(
575
+ (resolvedEl) => {
576
+ if (timer) clearTimeout(timer);
577
+ return { id, html: renderToString(resolvedEl) };
578
+ },
579
+ (err) => {
580
+ if (timer) clearTimeout(timer);
581
+ if (_isDev8) console.warn("[SibuJS SSR] ssrSuspense rejected:", err);
582
+ return { id, html: fallbackHtml };
583
+ }
584
+ );
585
+ promise.catch(noop);
493
586
  return { element: wrapper, promise };
494
587
  }
495
588
  function suspenseSwapScript(id, nonce) {
@@ -515,14 +608,26 @@ async function* renderToSuspenseStream(element, pendingBoundaries = [], options)
515
608
  function escapeScriptJson(json) {
516
609
  return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
517
610
  }
518
- function serializeState(state, nonce) {
519
- const json = escapeScriptJson(JSON.stringify(state));
611
+ function serializeState(state, nonce, options) {
612
+ const rawJson = JSON.stringify(state);
613
+ const maxBytes = options?.maxBytes ?? DEFAULT_MAX_SSR_BYTES;
614
+ const byteLen = typeof TextEncoder !== "undefined" ? new TextEncoder().encode(rawJson).byteLength : Buffer.byteLength(rawJson, "utf8");
615
+ if (byteLen > maxBytes) {
616
+ throw new Error(`[SibuJS SSR] serializeState: payload (${byteLen} bytes) exceeds maxBytes (${maxBytes})`);
617
+ }
618
+ const json = escapeScriptJson(rawJson);
520
619
  const nonceAttr = nonce ? ` nonce="${escapeAttr(nonce)}"` : "";
521
620
  return `<script${nonceAttr}>window.${SSR_DATA_ATTR}=${json}</script>`;
522
621
  }
523
622
  function deserializeState(validate) {
524
623
  if (typeof window === "undefined") return void 0;
525
- const raw = window[SSR_DATA_ATTR];
624
+ if (_isDev8 && !validate) {
625
+ console.warn(
626
+ "[SibuJS SSR] deserializeState() called without a validate guard \u2014 tampered SSR payloads will not be detected."
627
+ );
628
+ }
629
+ const w = window;
630
+ const raw = w[SSR_DATA_ATTR];
526
631
  if (raw === void 0) return void 0;
527
632
  if (validate && !validate(raw)) return void 0;
528
633
  return raw;
@@ -533,13 +638,14 @@ function escapeHtml(str) {
533
638
  function escapeAttr(str) {
534
639
  return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
535
640
  }
536
- var _isDev7, SAFE_ATTR_NAME, URL_ATTRS, VOID_ELEMENTS, suspenseIdCounter, SAFE_SUSPENSE_ID, SSR_DATA_ATTR;
641
+ var _isDev8, SAFE_ATTR_NAME, URL_ATTRS, VOID_ELEMENTS, SAFE_ID, SAFE_SUSPENSE_ID, SSR_DATA_ATTR, DEFAULT_MAX_SSR_BYTES;
537
642
  var init_ssr = __esm({
538
643
  "src/platform/ssr.ts"() {
539
644
  "use strict";
540
645
  init_dev();
646
+ init_ssr_context();
541
647
  init_sanitize();
542
- _isDev7 = isDev();
648
+ _isDev8 = isDev();
543
649
  SAFE_ATTR_NAME = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
544
650
  URL_ATTRS = /* @__PURE__ */ new Set([
545
651
  "href",
@@ -571,9 +677,10 @@ var init_ssr = __esm({
571
677
  "track",
572
678
  "wbr"
573
679
  ]);
574
- suspenseIdCounter = 0;
680
+ SAFE_ID = /^[A-Za-z0-9_-]+$/;
575
681
  SAFE_SUSPENSE_ID = /^[A-Za-z0-9_-]+$/;
576
682
  SSR_DATA_ATTR = "__SIBU_SSR_DATA__";
683
+ DEFAULT_MAX_SSR_BYTES = 1024 * 1024;
577
684
  }
578
685
  });
579
686
 
@@ -588,6 +695,7 @@ __export(plugins_exports, {
588
695
  Suspense: () => Suspense,
589
696
  Trans: () => Trans,
590
697
  VERSION: () => VERSION,
698
+ __removeRouterPagehideHandler: () => __removeRouterPagehideHandler,
591
699
  addRoute: () => addRoute,
592
700
  afterEach: () => afterEach,
593
701
  back: () => back,
@@ -603,6 +711,7 @@ __export(plugins_exports, {
603
711
  createMigrationRunner: () => createMigrationRunner,
604
712
  createModuleRegistry: () => createModuleRegistry,
605
713
  createPlugin: () => createPlugin,
714
+ createPluginRegistry: () => createPluginRegistry,
606
715
  createRouter: () => createRouter,
607
716
  createSSRCache: () => createSSRCache,
608
717
  createSSRRouter: () => createSSRRouter,
@@ -645,6 +754,7 @@ __export(plugins_exports, {
645
754
  routerState: () => routerState,
646
755
  satisfies: () => satisfies,
647
756
  serializeRouteState: () => serializeRouteState,
757
+ setDefaultPluginRegistry: () => setDefaultPluginRegistry,
648
758
  setLocale: () => setLocale,
649
759
  setRouteTransition: () => setRouteTransition,
650
760
  setRoutes: () => setRoutes,
@@ -655,6 +765,9 @@ __export(plugins_exports, {
655
765
  });
656
766
  module.exports = __toCommonJS(plugins_exports);
657
767
 
768
+ // src/core/rendering/tagFactory.ts
769
+ init_dev();
770
+
658
771
  // src/reactivity/bindAttribute.ts
659
772
  init_dev();
660
773
  init_sanitize();
@@ -662,15 +775,28 @@ init_sanitize();
662
775
  // src/reactivity/track.ts
663
776
  init_dev();
664
777
  var _isDev2 = isDev();
665
- var subscriberStack = new Array(32);
666
- var stackCapacity = 32;
778
+ var STACK_INITIAL = 32;
779
+ var STACK_SHRINK_THRESHOLD = 128;
780
+ var subscriberStack = new Array(STACK_INITIAL);
781
+ var stackCapacity = STACK_INITIAL;
667
782
  var stackTop = -1;
668
783
  var currentSubscriber = null;
669
- var signalSubscribers = /* @__PURE__ */ new WeakMap();
670
784
  var SUBS = "__s";
785
+ function syncFastPath(signal2, subs) {
786
+ const size = subs.size;
787
+ if (size === 0) {
788
+ signal2.__f = void 0;
789
+ delete signal2[SUBS];
790
+ } else if (size === 1) {
791
+ signal2.__f = subs.values().next().value;
792
+ } else {
793
+ signal2.__f = void 0;
794
+ }
795
+ }
671
796
  var notifyDepth = 0;
672
797
  var pendingQueue = [];
673
798
  var pendingSet = /* @__PURE__ */ new Set();
799
+ var propagateStack = [];
674
800
  function safeInvoke(sub2) {
675
801
  try {
676
802
  sub2();
@@ -693,75 +819,133 @@ function track(effectFn, subscriber) {
693
819
  } finally {
694
820
  stackTop--;
695
821
  currentSubscriber = stackTop >= 0 ? subscriberStack[stackTop] : null;
822
+ if (stackTop < 0 && stackCapacity > STACK_SHRINK_THRESHOLD) {
823
+ stackCapacity = Math.max(STACK_INITIAL, stackCapacity >>> 1);
824
+ subscriberStack.length = stackCapacity;
825
+ }
696
826
  }
697
827
  return () => cleanup(subscriber);
698
828
  }
699
829
  function recordDependency(signal2) {
700
830
  if (!currentSubscriber) return;
701
831
  const sub2 = currentSubscriber;
702
- if (sub2._dep === signal2) return;
832
+ const epoch = sub2._epoch;
833
+ if (sub2._dep === signal2) {
834
+ sub2._depEpoch = epoch;
835
+ return;
836
+ }
703
837
  const deps = sub2._deps;
704
838
  if (deps) {
705
- if (deps.has(signal2)) return;
706
- deps.add(signal2);
839
+ deps.set(signal2, epoch);
707
840
  } else if (sub2._dep !== void 0) {
708
- const set = /* @__PURE__ */ new Set();
709
- set.add(sub2._dep);
710
- set.add(signal2);
711
- sub2._deps = set;
841
+ const map2 = /* @__PURE__ */ new Map();
842
+ map2.set(sub2._dep, sub2._depEpoch);
843
+ map2.set(signal2, epoch);
844
+ sub2._deps = map2;
712
845
  sub2._dep = void 0;
846
+ sub2._depEpoch = void 0;
713
847
  } else {
714
848
  sub2._dep = signal2;
849
+ sub2._depEpoch = epoch;
715
850
  }
716
- let subs = signal2[SUBS];
851
+ const sig = signal2;
852
+ let subs = sig[SUBS];
717
853
  if (!subs) {
718
854
  subs = /* @__PURE__ */ new Set();
719
- signalSubscribers.set(signal2, subs);
720
- signal2[SUBS] = subs;
855
+ sig[SUBS] = subs;
721
856
  }
857
+ const prevSize = subs.size;
722
858
  subs.add(currentSubscriber);
723
- if (subs.size === 1) {
724
- signal2.__f = currentSubscriber;
725
- } else if (signal2.__f !== void 0) {
726
- signal2.__f = void 0;
859
+ if (subs.size !== prevSize) {
860
+ if (subs.size === 1) {
861
+ sig.__f = currentSubscriber;
862
+ } else if (sig.__f !== void 0) {
863
+ sig.__f = void 0;
864
+ }
865
+ }
866
+ }
867
+ var maxSubscriberRepeats = 50;
868
+ var maxDrainIterations = 1e6;
869
+ var drainEpoch = 0;
870
+ function tickRepeat(sub2) {
871
+ const s2 = sub2;
872
+ if (s2._runEpoch !== drainEpoch) {
873
+ s2._runEpoch = drainEpoch;
874
+ s2._runs = 1;
875
+ return false;
876
+ }
877
+ return ++s2._runs > maxSubscriberRepeats;
878
+ }
879
+ function cycleError(sub2) {
880
+ if (typeof console !== "undefined") {
881
+ const name = sub2.__name ?? "<unnamed>";
882
+ console.error(
883
+ `[SibuJS] subscriber "${name}" fired more than ${maxSubscriberRepeats} times \u2014 likely a write-reads-self cycle between effects/signals. Breaking to prevent infinite loop.`
884
+ );
885
+ }
886
+ }
887
+ function absoluteDrainError() {
888
+ if (typeof console !== "undefined") {
889
+ console.error(
890
+ `[SibuJS] Notification drain exceeded ${maxDrainIterations} iterations \u2014 absolute safety net tripped. Breaking to prevent infinite loop.`
891
+ );
892
+ }
893
+ }
894
+ function drainQueue() {
895
+ let i2 = 0;
896
+ while (i2 < pendingQueue.length) {
897
+ if (i2 >= maxDrainIterations) {
898
+ absoluteDrainError();
899
+ break;
900
+ }
901
+ const sub2 = pendingQueue[i2++];
902
+ if (tickRepeat(sub2)) {
903
+ cycleError(sub2);
904
+ break;
905
+ }
906
+ pendingSet.delete(sub2);
907
+ safeInvoke(sub2);
727
908
  }
728
909
  }
729
910
  function propagateDirty(sub2) {
730
911
  sub2();
731
- let sig = sub2._sig;
732
- while (sig) {
912
+ const rootSig = sub2._sig;
913
+ if (!rootSig) return;
914
+ const stack = propagateStack;
915
+ const baseLen = stack.length;
916
+ stack.push(rootSig);
917
+ while (stack.length > baseLen) {
918
+ const sig = stack.pop();
733
919
  const first = sig.__f;
734
920
  if (first) {
735
921
  if (first._c) {
736
922
  const nSig = first._sig;
737
- nSig._d = true;
738
- sig = nSig;
739
- continue;
740
- }
741
- if (!pendingSet.has(first)) {
923
+ if (!nSig._d) {
924
+ nSig._d = true;
925
+ stack.push(nSig);
926
+ }
927
+ } else if (!pendingSet.has(first)) {
742
928
  pendingSet.add(first);
743
929
  pendingQueue.push(first);
744
930
  }
745
- break;
931
+ continue;
746
932
  }
747
933
  const subs = sig[SUBS];
748
- if (!subs) break;
749
- let nextSig;
934
+ if (!subs) continue;
750
935
  for (const s2 of subs) {
751
936
  if (s2._c) {
752
- s2();
753
937
  const nSig = s2._sig;
754
- if (nSig && !nextSig) {
755
- nextSig = nSig;
756
- } else if (nSig) {
757
- propagateDirty(s2);
938
+ if (nSig && !nSig._d) {
939
+ nSig._d = true;
940
+ stack.push(nSig);
941
+ } else if (!nSig) {
942
+ s2();
758
943
  }
759
944
  } else if (!pendingSet.has(s2)) {
760
945
  pendingSet.add(s2);
761
946
  pendingQueue.push(s2);
762
947
  }
763
948
  }
764
- sig = nextSig;
765
949
  }
766
950
  }
767
951
  function notifySubscribers(signal2) {
@@ -777,21 +961,22 @@ function notifySubscribers(signal2) {
777
961
  return;
778
962
  }
779
963
  notifyDepth++;
964
+ drainEpoch++;
780
965
  try {
781
966
  if (first._c) {
782
967
  propagateDirty(first);
968
+ } else if (tickRepeat(first)) {
969
+ cycleError(first);
783
970
  } else {
784
971
  safeInvoke(first);
785
972
  }
786
- let i2 = 0;
787
- while (i2 < pendingQueue.length) {
788
- safeInvoke(pendingQueue[i2]);
789
- i2++;
790
- }
973
+ drainQueue();
791
974
  } finally {
792
- pendingQueue.length = 0;
793
- pendingSet.clear();
794
975
  notifyDepth--;
976
+ if (notifyDepth === 0) {
977
+ pendingQueue.length = 0;
978
+ pendingSet.clear();
979
+ }
795
980
  }
796
981
  return;
797
982
  }
@@ -809,57 +994,45 @@ function notifySubscribers(signal2) {
809
994
  return;
810
995
  }
811
996
  notifyDepth++;
997
+ drainEpoch++;
812
998
  try {
813
- let directCount = 0;
814
999
  for (const sub2 of subs) {
815
- pendingQueue[directCount++] = sub2;
816
- }
817
- for (let i3 = 0; i3 < directCount; i3++) {
818
- if (pendingQueue[i3]._c) {
819
- propagateDirty(pendingQueue[i3]);
820
- }
821
- }
822
- for (let i3 = 0; i3 < directCount; i3++) {
823
- if (!pendingQueue[i3]._c) {
824
- if (!pendingSet.has(pendingQueue[i3])) {
825
- safeInvoke(pendingQueue[i3]);
826
- }
1000
+ if (sub2._c) {
1001
+ propagateDirty(sub2);
1002
+ } else if (!pendingSet.has(sub2)) {
1003
+ pendingSet.add(sub2);
1004
+ pendingQueue.push(sub2);
827
1005
  }
828
1006
  }
829
- let i2 = directCount;
830
- while (i2 < pendingQueue.length) {
831
- safeInvoke(pendingQueue[i2]);
832
- i2++;
833
- }
1007
+ drainQueue();
834
1008
  } finally {
835
- pendingQueue.length = 0;
836
- pendingSet.clear();
837
1009
  notifyDepth--;
1010
+ if (notifyDepth === 0) {
1011
+ pendingQueue.length = 0;
1012
+ pendingSet.clear();
1013
+ }
838
1014
  }
839
1015
  }
840
1016
  function cleanup(subscriber) {
841
1017
  const sub2 = subscriber;
842
1018
  const singleDep = sub2._dep;
843
1019
  if (singleDep !== void 0) {
844
- const subs = singleDep[SUBS];
845
- if (subs) {
846
- subs.delete(subscriber);
847
- if (singleDep.__f === subscriber) {
848
- singleDep.__f = void 0;
849
- }
1020
+ const sig = singleDep;
1021
+ const subs = sig[SUBS];
1022
+ if (subs?.delete(subscriber)) {
1023
+ syncFastPath(sig, subs);
850
1024
  }
851
1025
  sub2._dep = void 0;
1026
+ sub2._depEpoch = void 0;
852
1027
  return;
853
1028
  }
854
1029
  const deps = sub2._deps;
855
1030
  if (!deps || deps.size === 0) return;
856
- for (const signal2 of deps) {
857
- const subs = signal2[SUBS];
858
- if (subs) {
859
- subs.delete(subscriber);
860
- if (signal2.__f === subscriber) {
861
- signal2.__f = void 0;
862
- }
1031
+ for (const signal2 of deps.keys()) {
1032
+ const sig = signal2;
1033
+ const subs = sig[SUBS];
1034
+ if (subs?.delete(subscriber)) {
1035
+ syncFastPath(sig, subs);
863
1036
  }
864
1037
  }
865
1038
  deps.clear();
@@ -867,6 +1040,9 @@ function cleanup(subscriber) {
867
1040
 
868
1041
  // src/reactivity/bindAttribute.ts
869
1042
  var _isDev3 = isDev();
1043
+ function setProp(el, key, val) {
1044
+ el[key] = val;
1045
+ }
870
1046
  function isEventHandlerAttr(name) {
871
1047
  if (name.length < 3) return false;
872
1048
  const lower = name.toLowerCase();
@@ -892,7 +1068,7 @@ function bindAttribute(el, attr, getter) {
892
1068
  }
893
1069
  if (typeof value === "boolean") {
894
1070
  if (attr in el && (attr === "checked" || attr === "disabled" || attr === "selected")) {
895
- el[attr] = value;
1071
+ setProp(el, attr, value);
896
1072
  } else if (value) {
897
1073
  el.setAttribute(attr, "");
898
1074
  } else {
@@ -902,7 +1078,7 @@ function bindAttribute(el, attr, getter) {
902
1078
  }
903
1079
  const str = String(value);
904
1080
  if ((attr === "value" || attr === "checked") && attr in el) {
905
- el[attr] = attr === "checked" ? Boolean(value) : str;
1081
+ setProp(el, attr, attr === "checked" ? Boolean(value) : str);
906
1082
  } else {
907
1083
  el.setAttribute(attr, isUrlAttribute(attr) ? sanitizeUrl(str) : str);
908
1084
  }
@@ -940,24 +1116,29 @@ function bindChildNode(placeholder, getter) {
940
1116
  let newNodes;
941
1117
  if (Array.isArray(result)) {
942
1118
  newNodes = [];
1119
+ const seen = /* @__PURE__ */ new Set();
943
1120
  for (let i2 = 0; i2 < result.length; i2++) {
944
1121
  const item = result[i2];
945
1122
  if (item == null || typeof item === "boolean") continue;
946
- newNodes.push(item instanceof Node ? item : document.createTextNode(String(item)));
1123
+ const node = item instanceof Node ? item : document.createTextNode(String(item));
1124
+ if (seen.has(node)) {
1125
+ if (_isDev4)
1126
+ devWarn("bindChildNode: duplicate node reference in array \u2014 only the first occurrence is rendered.");
1127
+ continue;
1128
+ }
1129
+ seen.add(node);
1130
+ newNodes.push(node);
947
1131
  }
948
1132
  } else {
949
1133
  const node = result instanceof Node ? result : document.createTextNode(String(result));
950
1134
  newNodes = [node];
951
1135
  }
952
- const reused = lastNodes.length > 0 && newNodes.length > 0 ? /* @__PURE__ */ new Set() : void 0;
953
- if (reused) {
1136
+ let reused;
1137
+ if (lastNodes.length > 0 && newNodes.length > 0) {
1138
+ const lastSet = new Set(lastNodes);
1139
+ reused = /* @__PURE__ */ new Set();
954
1140
  for (let i2 = 0; i2 < newNodes.length; i2++) {
955
- for (let j = 0; j < lastNodes.length; j++) {
956
- if (newNodes[i2] === lastNodes[j]) {
957
- reused.add(newNodes[i2]);
958
- break;
959
- }
960
- }
1141
+ if (lastSet.has(newNodes[i2])) reused.add(newNodes[i2]);
961
1142
  }
962
1143
  }
963
1144
  for (let i2 = 0; i2 < lastNodes.length; i2++) {
@@ -1004,7 +1185,7 @@ function dispose(node) {
1004
1185
  while (stack.length > 0) {
1005
1186
  const current = stack.pop();
1006
1187
  order.push(current);
1007
- const children = current.childNodes;
1188
+ const children = Array.from(current.childNodes);
1008
1189
  for (let i2 = 0; i2 < children.length; i2++) {
1009
1190
  stack.push(children[i2]);
1010
1191
  }
@@ -1013,8 +1194,10 @@ function dispose(node) {
1013
1194
  const current = order[i2];
1014
1195
  const disposers = elementDisposers.get(current);
1015
1196
  if (disposers) {
1016
- if (_isDev5) activeBindingCount -= disposers.length;
1017
- for (const d of disposers) {
1197
+ const snapshot = disposers.slice();
1198
+ elementDisposers.delete(current);
1199
+ if (_isDev5) activeBindingCount -= snapshot.length;
1200
+ for (const d of snapshot) {
1018
1201
  try {
1019
1202
  d();
1020
1203
  } catch (err) {
@@ -1023,13 +1206,53 @@ function dispose(node) {
1023
1206
  }
1024
1207
  }
1025
1208
  }
1026
- elementDisposers.delete(current);
1209
+ let extraPasses = 0;
1210
+ while (extraPasses++ < 8) {
1211
+ const added = elementDisposers.get(current);
1212
+ if (!added || added.length === 0) break;
1213
+ const moreSnapshot = added.slice();
1214
+ elementDisposers.delete(current);
1215
+ if (_isDev5) activeBindingCount -= moreSnapshot.length;
1216
+ for (const d of moreSnapshot) {
1217
+ try {
1218
+ d();
1219
+ } catch (err) {
1220
+ if (_isDev5 && typeof console !== "undefined") {
1221
+ console.warn("[SibuJS] Disposer threw during cleanup:", err);
1222
+ }
1223
+ }
1224
+ }
1225
+ }
1027
1226
  }
1028
1227
  }
1029
1228
  }
1030
1229
 
1031
1230
  // src/core/rendering/tagFactory.ts
1032
1231
  var SVG_NS = "http://www.w3.org/2000/svg";
1232
+ var _isDev6 = isDev();
1233
+ var BLOCKED_TAGS = /* @__PURE__ */ new Set(["script", "iframe", "object", "embed", "frame", "frameset"]);
1234
+ function validateTagName(tag) {
1235
+ const lower = tag.toLowerCase();
1236
+ if (BLOCKED_TAGS.has(lower)) {
1237
+ throw new Error(`tagFactory: refusing to create <${tag}> \u2014 tag is blocked for security reasons.`);
1238
+ }
1239
+ }
1240
+ var CLOBBER_RISKY_IDS = /* @__PURE__ */ new Set([
1241
+ "config",
1242
+ "location",
1243
+ "history",
1244
+ "document",
1245
+ "window",
1246
+ "navigator",
1247
+ "name",
1248
+ "top",
1249
+ "parent",
1250
+ "self",
1251
+ "frames"
1252
+ ]);
1253
+ function setProp2(el, key, val) {
1254
+ el[key] = val;
1255
+ }
1033
1256
  var kebabCache = /* @__PURE__ */ new Map();
1034
1257
  function toKebab(prop) {
1035
1258
  let cached = kebabCache.get(prop);
@@ -1154,79 +1377,103 @@ function appendChildren(el, nodes) {
1154
1377
  }
1155
1378
  }
1156
1379
  }
1157
- var tagFactory = (tag, ns) => (first, second) => {
1158
- const el = ns ? document.createElementNS(ns, tag) : document.createElement(tag);
1159
- if (first === void 0) return el;
1160
- if (typeof first === "string") {
1161
- if (second !== void 0) {
1162
- el.setAttribute("class", first);
1163
- appendChildren(el, second);
1380
+ var tagFactory = (tag, ns) => {
1381
+ return (first, second) => {
1382
+ validateTagName(tag);
1383
+ const el = ns ? document.createElementNS(ns, tag) : document.createElement(tag);
1384
+ if (first === void 0) return el;
1385
+ if (typeof first === "string") {
1386
+ if (second !== void 0) {
1387
+ el.setAttribute("class", first);
1388
+ appendChildren(el, second);
1389
+ return el;
1390
+ }
1391
+ el.textContent = first;
1164
1392
  return el;
1165
1393
  }
1166
- el.textContent = first;
1167
- return el;
1168
- }
1169
- if (typeof first === "number") {
1170
- el.textContent = String(first);
1171
- return el;
1172
- }
1173
- if (Array.isArray(first) || first instanceof Node || typeof first === "function") {
1174
- appendChildren(el, first);
1175
- return el;
1176
- }
1177
- const props = first;
1178
- const pClass = props.class;
1179
- if (pClass != null) applyClass(el, pClass);
1180
- const pId = props.id;
1181
- if (pId != null) el.id = pId;
1182
- const pNodes = second !== void 0 ? second : props.nodes;
1183
- if (pNodes != null) appendChildren(el, pNodes);
1184
- const pOn = props.on;
1185
- if (pOn) {
1186
- for (const ev in pOn) {
1187
- el.addEventListener(ev, pOn[ev]);
1188
- }
1189
- }
1190
- const pStyle = props.style;
1191
- if (pStyle != null) applyStyle(el, pStyle);
1192
- const pRef = props.ref;
1193
- if (pRef) pRef.current = el;
1194
- for (const key in props) {
1195
- switch (key) {
1196
- case "class":
1197
- case "id":
1198
- case "nodes":
1199
- case "on":
1200
- case "style":
1201
- case "ref":
1202
- case "onElement":
1203
- continue;
1204
- // already handled above / below
1205
- default: {
1206
- const value = props[key];
1207
- if (value == null) continue;
1208
- if (key[0] === "o" && key[1] === "n") continue;
1209
- if (typeof value === "function") {
1210
- registerDisposer(el, bindAttribute(el, key, value));
1211
- } else if (typeof value === "boolean") {
1212
- if (key in el && (key === "checked" || key === "disabled" || key === "selected")) {
1213
- el[key] = value;
1214
- } else if (value) {
1215
- el.setAttribute(key, "");
1394
+ if (typeof first === "number") {
1395
+ el.textContent = String(first);
1396
+ return el;
1397
+ }
1398
+ if (Array.isArray(first) || first instanceof Node || typeof first === "function") {
1399
+ appendChildren(el, first);
1400
+ return el;
1401
+ }
1402
+ const props = first;
1403
+ const pClass = props.class;
1404
+ if (pClass != null) applyClass(el, pClass);
1405
+ const pId = props.id;
1406
+ if (pId != null) {
1407
+ if (_isDev6 && typeof pId === "string" && CLOBBER_RISKY_IDS.has(pId.toLowerCase())) {
1408
+ devWarn(
1409
+ `tagFactory: element id="${pId}" matches a common global and may cause DOM clobbering. Avoid setting ids from untrusted input.`
1410
+ );
1411
+ }
1412
+ el.id = pId;
1413
+ }
1414
+ const pNodes = second !== void 0 ? second : props.nodes;
1415
+ if (pNodes != null) appendChildren(el, pNodes);
1416
+ const pOn = props.on;
1417
+ if (pOn) {
1418
+ for (const ev in pOn) {
1419
+ const handler = pOn[ev];
1420
+ if (typeof handler === "function") {
1421
+ el.addEventListener(ev, handler);
1422
+ } else if (_isDev6) {
1423
+ devWarn(
1424
+ `tagFactory: on.${ev} handler is not a function (got ${typeof handler}). Event listener was not attached.`
1425
+ );
1426
+ }
1427
+ }
1428
+ }
1429
+ const pStyle = props.style;
1430
+ if (pStyle != null) applyStyle(el, pStyle);
1431
+ const pRef = props.ref;
1432
+ if (pRef) pRef.current = el;
1433
+ for (const key in props) {
1434
+ switch (key) {
1435
+ case "class":
1436
+ case "id":
1437
+ case "nodes":
1438
+ case "on":
1439
+ case "style":
1440
+ case "ref":
1441
+ case "onElement":
1442
+ continue;
1443
+ // already handled above / below
1444
+ default: {
1445
+ const value = props[key];
1446
+ if (value == null) continue;
1447
+ const lkey = key.toLowerCase();
1448
+ if (lkey[0] === "o" && lkey[1] === "n") continue;
1449
+ if (typeof value === "function") {
1450
+ registerDisposer(el, bindAttribute(el, key, value));
1451
+ } else if (typeof value === "boolean") {
1452
+ if (key in el && (key === "checked" || key === "disabled" || key === "selected")) {
1453
+ setProp2(el, key, value);
1454
+ } else if (value) {
1455
+ el.setAttribute(key, "");
1456
+ } else {
1457
+ el.removeAttribute(key);
1458
+ }
1216
1459
  } else {
1217
- el.removeAttribute(key);
1460
+ const str = String(value);
1461
+ if (lkey === "srcset") {
1462
+ el.setAttribute(key, sanitizeSrcset(str));
1463
+ } else if (isUrlAttribute(lkey)) {
1464
+ el.setAttribute(key, sanitizeUrl(str));
1465
+ } else {
1466
+ el.setAttribute(key, str);
1467
+ }
1218
1468
  }
1219
- } else {
1220
- const str = String(value);
1221
- el.setAttribute(key, isUrlAttribute(key) ? sanitizeUrl(str) : str);
1222
1469
  }
1223
1470
  }
1224
1471
  }
1225
- }
1226
- if (props.onElement && typeof props.onElement === "function") {
1227
- props.onElement(el);
1228
- }
1229
- return el;
1472
+ if (props.onElement && typeof props.onElement === "function") {
1473
+ props.onElement(el);
1474
+ }
1475
+ return el;
1476
+ };
1230
1477
  };
1231
1478
 
1232
1479
  // src/core/rendering/html.ts
@@ -1379,10 +1626,10 @@ function enqueueBatchedSignal(signal2) {
1379
1626
  // src/core/signals/signal.ts
1380
1627
  init_dev();
1381
1628
  var _g = globalThis;
1382
- var _isDev6 = isDev();
1629
+ var _isDev7 = isDev();
1383
1630
  function signal(initial, options) {
1384
1631
  const state = { value: initial };
1385
- const debugName = _isDev6 ? options?.name : void 0;
1632
+ const debugName = _isDev7 ? options?.name : void 0;
1386
1633
  const equalsFn = options?.equals;
1387
1634
  if (debugName) {
1388
1635
  state.__name = debugName;
@@ -1396,7 +1643,7 @@ function signal(initial, options) {
1396
1643
  function set(next) {
1397
1644
  const newValue = typeof next === "function" ? next(state.value) : next;
1398
1645
  if (equalsFn ? equalsFn(state.value, newValue) : Object.is(newValue, state.value)) return;
1399
- if (_isDev6) {
1646
+ if (_isDev7) {
1400
1647
  const oldValue = state.value;
1401
1648
  state.value = newValue;
1402
1649
  const hook = _g.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
@@ -1408,7 +1655,7 @@ function signal(initial, options) {
1408
1655
  notifySubscribers(state);
1409
1656
  }
1410
1657
  }
1411
- if (_isDev6) {
1658
+ if (_isDev7) {
1412
1659
  const hook = _g.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1413
1660
  if (hook) hook.emit("signal:create", { signal: state, name: debugName, getter: get, initial });
1414
1661
  }
@@ -1445,40 +1692,121 @@ function getAvailableLocales() {
1445
1692
 
1446
1693
  // src/core/signals/effect.ts
1447
1694
  init_dev();
1448
-
1449
- // src/core/ssr-context.ts
1450
- var ssrMode = false;
1451
- function isSSR() {
1452
- return ssrMode;
1453
- }
1454
-
1455
- // src/core/signals/effect.ts
1695
+ init_ssr_context();
1456
1696
  var _g2 = globalThis;
1457
1697
  function effect(effectFn, options) {
1458
1698
  devAssert(typeof effectFn === "function", "effect: argument must be a function.");
1459
1699
  if (isSSR()) return () => {
1460
1700
  };
1461
1701
  const onError = options?.onError;
1702
+ let userCleanups = [];
1703
+ const onCleanup = (fn) => {
1704
+ userCleanups.push(fn);
1705
+ };
1706
+ const runUserCleanups = () => {
1707
+ if (userCleanups.length === 0) return;
1708
+ const list = userCleanups;
1709
+ userCleanups = [];
1710
+ for (let i2 = list.length - 1; i2 >= 0; i2--) {
1711
+ try {
1712
+ list[i2]();
1713
+ } catch (err) {
1714
+ if (typeof console !== "undefined") {
1715
+ console.warn("[SibuJS effect] onCleanup threw:", err);
1716
+ }
1717
+ }
1718
+ }
1719
+ };
1720
+ const invokeBody = () => effectFn(onCleanup);
1462
1721
  const wrappedFn = onError ? () => {
1463
1722
  try {
1464
- effectFn();
1723
+ invokeBody();
1465
1724
  } catch (err) {
1466
1725
  onError(err);
1467
1726
  }
1468
- } : effectFn;
1727
+ } : invokeBody;
1469
1728
  let cleanupHandle = () => {
1470
1729
  };
1730
+ let running = false;
1731
+ let rerunPending = false;
1732
+ const MAX_RERUNS = 100;
1471
1733
  const subscriber = () => {
1472
- cleanupHandle();
1473
- cleanupHandle = track(wrappedFn, subscriber);
1734
+ if (running) {
1735
+ rerunPending = true;
1736
+ return;
1737
+ }
1738
+ running = true;
1739
+ try {
1740
+ let reruns = 0;
1741
+ do {
1742
+ rerunPending = false;
1743
+ runUserCleanups();
1744
+ cleanupHandle();
1745
+ cleanupHandle = track(wrappedFn, subscriber);
1746
+ if (++reruns > MAX_RERUNS) {
1747
+ if (_g2.__SIBU_DEV_WARN__ !== false && typeof console !== "undefined") {
1748
+ console.error(
1749
+ `[SibuJS] effect re-requested itself ${MAX_RERUNS}+ times \u2014 likely a write-reads-self cycle. Breaking to prevent infinite loop.`
1750
+ );
1751
+ }
1752
+ rerunPending = false;
1753
+ break;
1754
+ }
1755
+ } while (rerunPending);
1756
+ } finally {
1757
+ running = false;
1758
+ rerunPending = false;
1759
+ }
1474
1760
  };
1475
- cleanupHandle = track(wrappedFn, subscriber);
1761
+ running = true;
1762
+ try {
1763
+ let reruns = 0;
1764
+ do {
1765
+ rerunPending = false;
1766
+ runUserCleanups();
1767
+ cleanupHandle();
1768
+ cleanupHandle = track(wrappedFn, subscriber);
1769
+ if (++reruns > MAX_RERUNS) {
1770
+ if (_g2.__SIBU_DEV_WARN__ !== false && typeof console !== "undefined") {
1771
+ console.error(
1772
+ `[SibuJS] effect re-requested itself ${MAX_RERUNS}+ times on initial run \u2014 likely a write-reads-self cycle. Breaking to prevent infinite loop.`
1773
+ );
1774
+ }
1775
+ rerunPending = false;
1776
+ break;
1777
+ }
1778
+ } while (rerunPending);
1779
+ } finally {
1780
+ running = false;
1781
+ rerunPending = false;
1782
+ }
1476
1783
  const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1477
1784
  if (hook) hook.emit("effect:create", { effectFn });
1785
+ let disposed = false;
1478
1786
  return () => {
1787
+ if (disposed) return;
1788
+ disposed = true;
1479
1789
  const h = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1480
- if (h) h.emit("effect:destroy", { effectFn });
1481
- cleanupHandle();
1790
+ if (h) {
1791
+ try {
1792
+ h.emit("effect:destroy", { effectFn });
1793
+ } catch {
1794
+ }
1795
+ }
1796
+ try {
1797
+ runUserCleanups();
1798
+ } catch (err) {
1799
+ if (typeof console !== "undefined") {
1800
+ console.warn("[SibuJS effect] onCleanup threw during dispose:", err);
1801
+ }
1802
+ }
1803
+ try {
1804
+ cleanupHandle();
1805
+ } catch (err) {
1806
+ if (typeof console !== "undefined") {
1807
+ console.warn("[SibuJS effect] dispose threw:", err);
1808
+ }
1809
+ }
1482
1810
  };
1483
1811
  }
1484
1812
 
@@ -1849,9 +2177,11 @@ var ComponentLoader = class {
1849
2177
  }
1850
2178
  return component;
1851
2179
  } catch (error) {
1852
- throw new Error(
2180
+ const wrapped = new Error(
1853
2181
  `Failed to load component for route "${routePath}": ${error instanceof Error ? error.message : String(error)}`
1854
2182
  );
2183
+ wrapped.cause = error;
2184
+ throw wrapped;
1855
2185
  }
1856
2186
  }
1857
2187
  isAsyncComponent(comp) {
@@ -2037,10 +2367,13 @@ var _SibuRouter = class _SibuRouter {
2037
2367
  }
2038
2368
  if ("redirect" in route2) {
2039
2369
  const redirectPath = typeof route2.redirect === "function" ? route2.redirect(to) : route2.redirect;
2040
- if (typeof redirectPath === "string" && /^https?:\/\/|^\/\//i.test(redirectPath)) {
2041
- console.warn(
2042
- `[SibuJS Router] Redirect to absolute URL "${redirectPath}" detected. Use relative paths for safer redirects.`
2043
- );
2370
+ if (typeof redirectPath === "string" && /^(https?:)?\/\//i.test(redirectPath)) {
2371
+ if (typeof console !== "undefined") {
2372
+ console.error(
2373
+ `[SibuJS Router] Refusing absolute/protocol-relative redirect "${redirectPath}" \u2014 open-redirect risk.`
2374
+ );
2375
+ }
2376
+ throw new NavigationFailureError("aborted", from, to);
2044
2377
  }
2045
2378
  if (typeof redirectPath === "string" && !isSafeNavigationTarget(redirectPath)) {
2046
2379
  throw new NavigationFailureError("aborted", from, to);
@@ -2243,6 +2576,7 @@ function createRouter(routesOrOptions, options = {}) {
2243
2576
  routes = [];
2244
2577
  }
2245
2578
  globalRouter = new SibuRouter(routes, options);
2579
+ ensureRouterPagehide();
2246
2580
  return globalRouter;
2247
2581
  }
2248
2582
  function setRoutes(routes) {
@@ -2316,7 +2650,9 @@ function Route() {
2316
2650
  let currentTopRoute = null;
2317
2651
  const cleanupNodes = () => {
2318
2652
  [currentNode, loadingNode, errorNode].forEach((node) => {
2319
- if (node?.parentNode) {
2653
+ if (!node) return;
2654
+ dispose(node);
2655
+ if (node.parentNode) {
2320
2656
  node.parentNode.removeChild(node);
2321
2657
  }
2322
2658
  });
@@ -2347,13 +2683,24 @@ function Route() {
2347
2683
  loadingNode = null;
2348
2684
  }
2349
2685
  };
2350
- const showError = (error) => {
2686
+ const showError = (error, routeDef) => {
2351
2687
  if (!anchor.parentNode) return;
2352
2688
  cleanupNodes();
2353
2689
  errorNode = document.createElement("div");
2354
2690
  errorNode.className = "route-error";
2355
2691
  errorNode.setAttribute("role", "alert");
2356
2692
  errorNode.setAttribute("aria-live", "assertive");
2693
+ if (routeDef && "component" in routeDef) {
2694
+ const src = routeDef.component.toString();
2695
+ const importMatch = src.match(/import\(["']([^"']+)["']\)/);
2696
+ if (importMatch) {
2697
+ errorNode.setAttribute("data-component-source", importMatch[1]);
2698
+ }
2699
+ if (routeDef.component.name) {
2700
+ errorNode.setAttribute("data-component-name", routeDef.component.name);
2701
+ }
2702
+ }
2703
+ errorNode.__routeError = error;
2357
2704
  const title2 = document.createElement("h3");
2358
2705
  title2.textContent = "Route Error";
2359
2706
  title2.className = "route-error-title";
@@ -2364,12 +2711,14 @@ function Route() {
2364
2711
  retryButton.textContent = "Retry";
2365
2712
  retryButton.className = "route-error-retry";
2366
2713
  retryButton.type = "button";
2367
- retryButton.addEventListener("click", () => {
2714
+ const onRetryClick = () => {
2368
2715
  if (globalRouter) {
2369
2716
  globalRouter.clearErrorCache();
2370
2717
  update();
2371
2718
  }
2372
- });
2719
+ };
2720
+ retryButton.addEventListener("click", onRetryClick);
2721
+ registerDisposer(retryButton, () => retryButton.removeEventListener("click", onRetryClick));
2373
2722
  errorNode.appendChild(title2);
2374
2723
  errorNode.appendChild(message);
2375
2724
  errorNode.appendChild(retryButton);
@@ -2401,7 +2750,11 @@ function Route() {
2401
2750
  currentTopRoute = routeDef;
2402
2751
  if ("redirect" in routeDef) {
2403
2752
  const redirectPath = typeof routeDef.redirect === "function" ? routeDef.redirect(route2) : routeDef.redirect;
2404
- queueMicrotask(() => globalRouter?.navigate(redirectPath));
2753
+ queueMicrotask(() => {
2754
+ globalRouter?.navigate(redirectPath).catch((err) => {
2755
+ if (typeof console !== "undefined") console.error("[router] redirect failed:", err);
2756
+ });
2757
+ });
2405
2758
  return;
2406
2759
  }
2407
2760
  if ("component" in routeDef) {
@@ -2420,7 +2773,7 @@ function Route() {
2420
2773
  } catch (error) {
2421
2774
  hideLoading();
2422
2775
  console.error("[Route] Component error:", error);
2423
- showError(error instanceof Error ? error : new Error(String(error)));
2776
+ showError(error instanceof Error ? error : new Error(String(error)), routeDef);
2424
2777
  }
2425
2778
  }
2426
2779
  } catch (error) {
@@ -2440,13 +2793,16 @@ function Route() {
2440
2793
  await originalUpdate();
2441
2794
  routeInitialized = true;
2442
2795
  };
2443
- track(wrappedUpdate);
2796
+ const routeTeardown = track(wrappedUpdate);
2444
2797
  if (!routeInitialized) {
2445
2798
  queueMicrotask(() => {
2446
2799
  if (!routeInitialized && anchor.parentNode) wrappedUpdate();
2447
2800
  });
2448
2801
  }
2449
- routeCleanups.push(cleanupNodes);
2802
+ routeCleanups.push(() => {
2803
+ routeTeardown();
2804
+ cleanupNodes();
2805
+ });
2450
2806
  return anchor;
2451
2807
  }
2452
2808
  function KeepAliveRoute(options) {
@@ -2475,7 +2831,11 @@ function KeepAliveRoute(options) {
2475
2831
  const { route: routeDef } = match;
2476
2832
  if ("redirect" in routeDef) {
2477
2833
  const redirectPath = typeof routeDef.redirect === "function" ? routeDef.redirect(route2) : routeDef.redirect;
2478
- queueMicrotask(() => globalRouter?.navigate(redirectPath));
2834
+ queueMicrotask(() => {
2835
+ globalRouter?.navigate(redirectPath).catch((err) => {
2836
+ if (typeof console !== "undefined") console.error("[router] redirect failed:", err);
2837
+ });
2838
+ });
2479
2839
  return;
2480
2840
  }
2481
2841
  if (!("component" in routeDef)) return;
@@ -2545,13 +2905,14 @@ function KeepAliveRoute(options) {
2545
2905
  await update();
2546
2906
  initialized = true;
2547
2907
  };
2548
- track(wrappedUpdate);
2908
+ const kaTeardown = track(wrappedUpdate);
2549
2909
  if (!initialized) {
2550
2910
  queueMicrotask(() => {
2551
2911
  if (!initialized && anchor.parentNode) wrappedUpdate();
2552
2912
  });
2553
2913
  }
2554
2914
  routeCleanups.push(() => {
2915
+ kaTeardown();
2555
2916
  for (const node of cache.values()) {
2556
2917
  dispose(node);
2557
2918
  if (node.parentNode) node.parentNode.removeChild(node);
@@ -2618,12 +2979,18 @@ function RouterLink(props) {
2618
2979
  }
2619
2980
  });
2620
2981
  }
2621
- link2.addEventListener("click", (e) => {
2982
+ const onLinkClick = (e) => {
2622
2983
  if (target || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) {
2623
2984
  return;
2624
2985
  }
2625
2986
  e.preventDefault();
2626
- globalRouter?.navigate(to, { replace: replace2 });
2987
+ globalRouter?.navigate(to, { replace: replace2 }).catch((err) => {
2988
+ if (typeof console !== "undefined") console.error("[router] link navigate failed:", err);
2989
+ });
2990
+ };
2991
+ link2.addEventListener("click", onLinkClick);
2992
+ registerDisposer(link2, () => {
2993
+ link2.removeEventListener("click", onLinkClick);
2627
2994
  });
2628
2995
  return link2;
2629
2996
  }
@@ -2734,10 +3101,21 @@ function destroyRouter() {
2734
3101
  globalRouter = null;
2735
3102
  }
2736
3103
  }
2737
- if (typeof window !== "undefined") {
2738
- window.addEventListener("beforeunload", () => {
2739
- destroyRouter();
2740
- });
3104
+ var _routerPagehideHandler = null;
3105
+ function ensureRouterPagehide() {
3106
+ if (_routerPagehideHandler || typeof window === "undefined") return;
3107
+ _routerPagehideHandler = (event) => {
3108
+ if (event.persisted === false) {
3109
+ destroyRouter();
3110
+ }
3111
+ };
3112
+ window.addEventListener("pagehide", _routerPagehideHandler);
3113
+ }
3114
+ function __removeRouterPagehideHandler() {
3115
+ if (_routerPagehideHandler && typeof window !== "undefined") {
3116
+ window.removeEventListener("pagehide", _routerPagehideHandler);
3117
+ _routerPagehideHandler = null;
3118
+ }
2741
3119
  }
2742
3120
  function Outlet() {
2743
3121
  const anchor = document.createComment("route-outlet-nested");
@@ -2763,12 +3141,20 @@ function Outlet() {
2763
3141
  console.error("[Outlet] Failed to render child route:", error);
2764
3142
  }
2765
3143
  };
2766
- track(update);
3144
+ const outletTeardown = track(update);
2767
3145
  if (!anchor.parentNode) {
2768
3146
  queueMicrotask(() => {
2769
3147
  if (anchor.parentNode) update();
2770
3148
  });
2771
3149
  }
3150
+ routeCleanups.push(() => {
3151
+ outletTeardown();
3152
+ if (currentNode) {
3153
+ dispose(currentNode);
3154
+ if (currentNode.parentNode) currentNode.parentNode.removeChild(currentNode);
3155
+ currentNode = null;
3156
+ }
3157
+ });
2772
3158
  return anchor;
2773
3159
  }
2774
3160
  function addRoute(route2, parentPath) {
@@ -3086,6 +3472,10 @@ function hydrateRouter(routes, options) {
3086
3472
  if (resolved.component) {
3087
3473
  hydrate2(resolved.component, container);
3088
3474
  }
3475
+ }).catch((err) => {
3476
+ if (typeof console !== "undefined") {
3477
+ console.error("[SibuJS routerSSR] failed to load hydrate:", err);
3478
+ }
3089
3479
  });
3090
3480
  }
3091
3481
  }
@@ -3159,83 +3549,117 @@ function escapeAttrLocal(str) {
3159
3549
  }
3160
3550
 
3161
3551
  // src/plugins/plugin.ts
3162
- var installedPlugins = /* @__PURE__ */ new Set();
3163
- var hooks = {
3164
- init: [],
3165
- mount: [],
3166
- unmount: [],
3167
- error: []
3168
- };
3169
- var provided = /* @__PURE__ */ new Map();
3552
+ function createPluginRegistry() {
3553
+ const installedPlugins = /* @__PURE__ */ new Set();
3554
+ const hooks = { init: [], mount: [], unmount: [], error: [] };
3555
+ const provided = /* @__PURE__ */ new Map();
3556
+ const registry = {
3557
+ installedPlugins,
3558
+ hooks,
3559
+ provided,
3560
+ plugin(p2, options) {
3561
+ if (installedPlugins.has(p2.name)) {
3562
+ console.warn(`[Plugin] "${p2.name}" is already installed.`);
3563
+ return;
3564
+ }
3565
+ const ctx = {
3566
+ onInit: (cb) => hooks.init.push(cb),
3567
+ onMount: (cb) => hooks.mount.push(cb),
3568
+ onUnmount: (cb) => hooks.unmount.push(cb),
3569
+ onError: (cb) => hooks.error.push(cb),
3570
+ provide: (key, value) => provided.set(key, value)
3571
+ };
3572
+ const initHooksBefore = hooks.init.length;
3573
+ p2.install(ctx, options);
3574
+ installedPlugins.add(p2.name);
3575
+ const justAdded = hooks.init.slice(initHooksBefore);
3576
+ for (const cb of justAdded) {
3577
+ try {
3578
+ cb();
3579
+ } catch (e) {
3580
+ console.error(`[Plugin] "${p2.name}" init error:`, e);
3581
+ }
3582
+ }
3583
+ },
3584
+ inject(key, defaultValue) {
3585
+ if (provided.has(key)) return provided.get(key);
3586
+ if (defaultValue !== void 0) return defaultValue;
3587
+ throw new Error(`[Plugin] No provider found for key "${key}"`);
3588
+ },
3589
+ triggerMount(element) {
3590
+ const snapshot = hooks.mount.slice();
3591
+ for (const hook of snapshot) {
3592
+ try {
3593
+ hook(element);
3594
+ } catch (e) {
3595
+ console.error("[Plugin] Mount hook error:", e);
3596
+ }
3597
+ }
3598
+ },
3599
+ triggerUnmount(element) {
3600
+ const snapshot = hooks.unmount.slice();
3601
+ for (const hook of snapshot) {
3602
+ try {
3603
+ hook(element);
3604
+ } catch (e) {
3605
+ console.error("[Plugin] Unmount hook error:", e);
3606
+ }
3607
+ }
3608
+ },
3609
+ triggerError(error) {
3610
+ const snapshot = hooks.error.slice();
3611
+ for (const hook of snapshot) {
3612
+ try {
3613
+ hook(error);
3614
+ } catch (e) {
3615
+ console.error("[Plugin] Error hook error:", e);
3616
+ }
3617
+ }
3618
+ },
3619
+ reset() {
3620
+ installedPlugins.clear();
3621
+ hooks.init.length = 0;
3622
+ hooks.mount.length = 0;
3623
+ hooks.unmount.length = 0;
3624
+ hooks.error.length = 0;
3625
+ provided.clear();
3626
+ }
3627
+ };
3628
+ return registry;
3629
+ }
3630
+ var defaultRegistry = createPluginRegistry();
3631
+ var defaultRegistryTouched = false;
3170
3632
  function createPlugin(name, install) {
3171
3633
  return { name, install };
3172
3634
  }
3173
3635
  function plugin(plugin2, options) {
3174
- if (installedPlugins.has(plugin2.name)) {
3175
- console.warn(`[Plugin] "${plugin2.name}" is already installed.`);
3176
- return;
3177
- }
3178
- const ctx = {
3179
- onInit: (cb) => hooks.init.push(cb),
3180
- onMount: (cb) => hooks.mount.push(cb),
3181
- onUnmount: (cb) => hooks.unmount.push(cb),
3182
- onError: (cb) => hooks.error.push(cb),
3183
- provide: (key, value) => provided.set(key, value)
3184
- };
3185
- const initHooksBefore = hooks.init.length;
3186
- plugin2.install(ctx, options);
3187
- installedPlugins.add(plugin2.name);
3188
- for (let i2 = initHooksBefore; i2 < hooks.init.length; i2++) {
3189
- try {
3190
- hooks.init[i2]();
3191
- } catch (e) {
3192
- console.error(`[Plugin] "${plugin2.name}" init error:`, e);
3193
- }
3194
- }
3636
+ defaultRegistryTouched = true;
3637
+ defaultRegistry.plugin(plugin2, options);
3195
3638
  }
3196
3639
  function inject(key, defaultValue) {
3197
- if (provided.has(key)) {
3198
- return provided.get(key);
3199
- }
3200
- if (defaultValue !== void 0) {
3201
- return defaultValue;
3202
- }
3203
- throw new Error(`[Plugin] No provider found for key "${key}"`);
3640
+ return defaultRegistry.inject(key, defaultValue);
3204
3641
  }
3205
3642
  function triggerPluginMount(element) {
3206
- for (const hook of hooks.mount) {
3207
- try {
3208
- hook(element);
3209
- } catch (e) {
3210
- console.error("[Plugin] Mount hook error:", e);
3211
- }
3212
- }
3643
+ defaultRegistry.triggerMount(element);
3213
3644
  }
3214
3645
  function triggerPluginUnmount(element) {
3215
- for (const hook of hooks.unmount) {
3216
- try {
3217
- hook(element);
3218
- } catch (e) {
3219
- console.error("[Plugin] Unmount hook error:", e);
3220
- }
3221
- }
3646
+ defaultRegistry.triggerUnmount(element);
3222
3647
  }
3223
3648
  function triggerPluginError(error) {
3224
- for (const hook of hooks.error) {
3225
- try {
3226
- hook(error);
3227
- } catch (e) {
3228
- console.error("[Plugin] Error hook error:", e);
3229
- }
3230
- }
3649
+ defaultRegistry.triggerError(error);
3231
3650
  }
3232
3651
  function resetPlugins() {
3233
- installedPlugins.clear();
3234
- hooks.init.length = 0;
3235
- hooks.mount.length = 0;
3236
- hooks.unmount.length = 0;
3237
- hooks.error.length = 0;
3238
- provided.clear();
3652
+ defaultRegistry.reset();
3653
+ defaultRegistryTouched = false;
3654
+ }
3655
+ function setDefaultPluginRegistry(registry) {
3656
+ if (defaultRegistryTouched && defaultRegistry.installedPlugins.size > 0) {
3657
+ console.warn(
3658
+ "[Plugin] Replacing default plugin registry while plugins are already installed on the singleton. This may indicate mixed singleton/registry usage."
3659
+ );
3660
+ }
3661
+ defaultRegistry = registry;
3662
+ defaultRegistryTouched = true;
3239
3663
  }
3240
3664
 
3241
3665
  // src/plugins/modular.ts
@@ -3713,7 +4137,8 @@ init_ssr();
3713
4137
  function preloadCritical(resources) {
3714
4138
  if (typeof document === "undefined") return;
3715
4139
  for (const resource of resources) {
3716
- const existing = document.querySelector(`link[rel="preload"][href="${resource.href}"]`);
4140
+ const safeHref = typeof CSS !== "undefined" && typeof CSS.escape === "function" ? CSS.escape(resource.href) : resource.href.replace(/["\\]/g, "\\$&");
4141
+ const existing = document.querySelector(`link[rel="preload"][href="${safeHref}"]`);
3717
4142
  if (existing) continue;
3718
4143
  const link2 = document.createElement("link");
3719
4144
  link2.rel = "preload";
@@ -3948,6 +4373,7 @@ function createBootSequence() {
3948
4373
  Suspense,
3949
4374
  Trans,
3950
4375
  VERSION,
4376
+ __removeRouterPagehideHandler,
3951
4377
  addRoute,
3952
4378
  afterEach,
3953
4379
  back,
@@ -3963,6 +4389,7 @@ function createBootSequence() {
3963
4389
  createMigrationRunner,
3964
4390
  createModuleRegistry,
3965
4391
  createPlugin,
4392
+ createPluginRegistry,
3966
4393
  createRouter,
3967
4394
  createSSRCache,
3968
4395
  createSSRRouter,
@@ -4005,6 +4432,7 @@ function createBootSequence() {
4005
4432
  routerState,
4006
4433
  satisfies,
4007
4434
  serializeRouteState,
4435
+ setDefaultPluginRegistry,
4008
4436
  setLocale,
4009
4437
  setRouteTransition,
4010
4438
  setRoutes,