sibujs 3.4.1 → 4.0.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 (98) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +118 -15
  3. package/dist/browser.cjs +410 -101
  4. package/dist/browser.d.cts +50 -5
  5. package/dist/browser.d.ts +50 -5
  6. package/dist/browser.js +8 -6
  7. package/dist/build.cjs +777 -385
  8. package/dist/build.js +15 -15
  9. package/dist/cdn.global.js +11 -7
  10. package/dist/{chunk-WEZ5SP7L.js → chunk-3EKJK5FZ.js} +73 -34
  11. package/dist/chunk-4MFZMLUU.js +147 -0
  12. package/dist/chunk-52XFPGSN.js +123 -0
  13. package/dist/chunk-5INI7D2L.js +194 -0
  14. package/dist/{chunk-5MFF5EPT.js → chunk-6LG2T7LO.js} +5 -5
  15. package/dist/{chunk-BQLVTAUZ.js → chunk-ACBT6UZS.js} +57 -22
  16. package/dist/{chunk-F7HW2NI5.js → chunk-AW6HPJ3L.js} +1 -1
  17. package/dist/chunk-DHDZ7IVN.js +109 -0
  18. package/dist/{chunk-NXR2YFZB.js → chunk-DQVUBVNX.js} +16 -25
  19. package/dist/{chunk-7PNKW3FJ.js → chunk-DR7H6UGM.js} +232 -67
  20. package/dist/{chunk-CV7FAQ2R.js → chunk-E62UADKL.js} +9 -3
  21. package/dist/{chunk-VQZDHAEK.js → chunk-ES2GERY2.js} +1 -1
  22. package/dist/{chunk-FWFEBYQ6.js → chunk-EWEE7NBF.js} +115 -41
  23. package/dist/{chunk-PBZWJQNY.js → chunk-F64ZPCFE.js} +8 -8
  24. package/dist/{chunk-S2VW4NBA.js → chunk-GCSGGA3Q.js} +295 -198
  25. package/dist/{chunk-EX77FXTT.js → chunk-HYCCIYNS.js} +228 -51
  26. package/dist/{chunk-MPE75KOA.js → chunk-NF3LYCQH.js} +259 -124
  27. package/dist/chunk-O3QQ3XOL.js +61 -0
  28. package/dist/{chunk-X4JS367G.js → chunk-PBQEW5VW.js} +14 -10
  29. package/dist/{chunk-QHPUFNHZ.js → chunk-RRIDJQ5C.js} +3 -3
  30. package/dist/{chunk-S3NFJO6L.js → chunk-TIRZCERI.js} +36 -5
  31. package/dist/{chunk-3FFCUIGV.js → chunk-VFHIVEPF.js} +309 -188
  32. package/dist/chunk-VPP2FONR.js +142 -0
  33. package/dist/{chunk-335KJEN2.js → chunk-W55YHPEP.js} +5 -3
  34. package/dist/{chunk-Y22QAKJX.js → chunk-WN6WDFCB.js} +4 -4
  35. package/dist/{chunk-YDWP3ZOU.js → chunk-WOOMUJFT.js} +228 -86
  36. package/dist/{chunk-4I3M326N.js → chunk-Z2CE42DB.js} +19 -25
  37. package/dist/{contracts-ey_Qh8ef.d.ts → contracts-Hg6ITAEF.d.cts} +6 -6
  38. package/dist/{contracts-ey_Qh8ef.d.cts → contracts-Hg6ITAEF.d.ts} +6 -6
  39. package/dist/{customElement-CPfIrbvg.d.cts → customElement-CKLoO63w.d.cts} +73 -11
  40. package/dist/{customElement-CPfIrbvg.d.ts → customElement-CKLoO63w.d.ts} +73 -11
  41. package/dist/data.cjs +483 -205
  42. package/dist/data.d.cts +139 -7
  43. package/dist/data.d.ts +139 -7
  44. package/dist/data.js +9 -9
  45. package/dist/devtools.cjs +222 -87
  46. package/dist/devtools.js +8 -8
  47. package/dist/dispose-4FZ6OFXR.js +19 -0
  48. package/dist/ecosystem.cjs +419 -175
  49. package/dist/ecosystem.js +11 -11
  50. package/dist/extras.cjs +1981 -765
  51. package/dist/extras.d.cts +7 -7
  52. package/dist/extras.d.ts +7 -7
  53. package/dist/extras.js +29 -26
  54. package/dist/index.cjs +784 -385
  55. package/dist/index.d.cts +299 -28
  56. package/dist/index.d.ts +299 -28
  57. package/dist/index.js +33 -18
  58. package/dist/motion.cjs +180 -50
  59. package/dist/motion.d.cts +26 -0
  60. package/dist/motion.d.ts +26 -0
  61. package/dist/motion.js +5 -5
  62. package/dist/patterns.cjs +437 -170
  63. package/dist/patterns.d.cts +60 -13
  64. package/dist/patterns.d.ts +60 -13
  65. package/dist/patterns.js +7 -7
  66. package/dist/performance.cjs +333 -148
  67. package/dist/performance.d.cts +55 -7
  68. package/dist/performance.d.ts +55 -7
  69. package/dist/performance.js +9 -9
  70. package/dist/plugins.cjs +1371 -576
  71. package/dist/plugins.d.cts +227 -9
  72. package/dist/plugins.d.ts +227 -9
  73. package/dist/plugins.js +610 -321
  74. package/dist/{ssr-D62yFwuw.d.cts → ssr-BiPRdZ6n.d.cts} +44 -15
  75. package/dist/{ssr-D62yFwuw.d.ts → ssr-BiPRdZ6n.d.ts} +44 -15
  76. package/dist/{ssr-2PPULEK2.js → ssr-JFP5535P.js} +5 -4
  77. package/dist/ssr.cjs +1104 -443
  78. package/dist/ssr.d.cts +230 -20
  79. package/dist/ssr.d.ts +230 -20
  80. package/dist/ssr.js +15 -12
  81. package/dist/{startup-0Qv6aosO.d.cts → startup-BMpaiMhP.d.cts} +1 -1
  82. package/dist/{startup-0Qv6aosO.d.ts → startup-BMpaiMhP.d.ts} +1 -1
  83. package/dist/testing.cjs +205 -63
  84. package/dist/testing.d.cts +1 -1
  85. package/dist/testing.d.ts +1 -1
  86. package/dist/testing.js +8 -10
  87. package/dist/ui.cjs +640 -199
  88. package/dist/ui.d.cts +2 -2
  89. package/dist/ui.d.ts +2 -2
  90. package/dist/ui.js +10 -10
  91. package/dist/widgets.cjs +207 -82
  92. package/dist/widgets.js +9 -9
  93. package/package.json +12 -7
  94. package/dist/chunk-5VGSK6D2.js +0 -80
  95. package/dist/chunk-6CAJU5TP.js +0 -38
  96. package/dist/chunk-COY6PUD2.js +0 -34
  97. package/dist/chunk-DLBFEAIS.js +0 -96
  98. package/dist/chunk-L3GAGWCC.js +0 -111
package/dist/plugins.cjs CHANGED
@@ -48,19 +48,117 @@ var init_dev = __esm({
48
48
  }
49
49
  });
50
50
 
51
+ // src/core/errors.ts
52
+ function state() {
53
+ const g2 = globalThis;
54
+ const existing = g2[ERROR_STATE_KEY];
55
+ if (existing) return existing;
56
+ const created = { handler: null, handlerDepth: 0 };
57
+ g2[ERROR_STATE_KEY] = created;
58
+ return created;
59
+ }
60
+ function normalizeError(value) {
61
+ if (value instanceof Error) return value;
62
+ const message = typeof value === "string" ? value : `Non-Error value thrown: ${safeStringify(value)}`;
63
+ return new Error(message, { cause: value });
64
+ }
65
+ function safeStringify(value) {
66
+ if (typeof value === "symbol") return value.toString();
67
+ try {
68
+ return typeof value === "object" && value !== null ? JSON.stringify(value) : String(value);
69
+ } catch {
70
+ return Object.prototype.toString.call(value);
71
+ }
72
+ }
73
+ function describe(context) {
74
+ return context.name ? `${context.phase} "${context.name}"` : context.phase;
75
+ }
76
+ function untrackedRun(fn) {
77
+ const api = globalThis[REACTIVE_REGISTRY_KEY];
78
+ return api?.untracked ? api.untracked(fn) : fn();
79
+ }
80
+ function claimedByBoundary(error, context) {
81
+ const raw = context.node;
82
+ if (!raw || typeof CustomEvent === "undefined") return false;
83
+ const target = resolveEventTarget(raw);
84
+ if (!target) return false;
85
+ try {
86
+ const event = new CustomEvent(PROPAGATE_EVENT, {
87
+ bubbles: true,
88
+ cancelable: true,
89
+ detail: { error, context }
90
+ });
91
+ return target.dispatchEvent(event) === false;
92
+ } catch {
93
+ return false;
94
+ }
95
+ }
96
+ function resolveEventTarget(node) {
97
+ const ELEMENT_NODE = 1;
98
+ let current = node;
99
+ while (current) {
100
+ if (current.nodeType === ELEMENT_NODE && typeof current.dispatchEvent === "function") {
101
+ return current;
102
+ }
103
+ current = current.parentNode;
104
+ }
105
+ const self2 = node;
106
+ return typeof self2.dispatchEvent === "function" ? self2 : null;
107
+ }
108
+ function defaultReport(error, context) {
109
+ if (typeof console === "undefined") return;
110
+ console.error(`[SibuJS] Uncaught error during ${describe(context)}: ${error.message}`, error);
111
+ }
112
+ function reportError(error, context) {
113
+ const err = normalizeError(error);
114
+ untrackedRun(() => {
115
+ if (claimedByBoundary(err, context)) return;
116
+ const s2 = state();
117
+ const handler = s2.handler;
118
+ if (handler && s2.handlerDepth === 0) {
119
+ s2.handlerDepth++;
120
+ try {
121
+ handler(error instanceof Error ? error : err, context);
122
+ return;
123
+ } catch (handlerError) {
124
+ if (typeof console !== "undefined") {
125
+ const detail = handlerError instanceof Error ? handlerError.message : String(handlerError);
126
+ console.error(`[SibuJS] runtime error handler itself threw: ${detail}`, handlerError);
127
+ }
128
+ } finally {
129
+ s2.handlerDepth--;
130
+ }
131
+ }
132
+ defaultReport(err, context);
133
+ });
134
+ }
135
+ var ERROR_STATE_KEY, REACTIVE_REGISTRY_KEY, PROPAGATE_EVENT;
136
+ var init_errors = __esm({
137
+ "src/core/errors.ts"() {
138
+ "use strict";
139
+ ERROR_STATE_KEY = /* @__PURE__ */ Symbol.for("sibujs.runtime-errors.v1");
140
+ REACTIVE_REGISTRY_KEY = /* @__PURE__ */ Symbol.for("sibujs.reactive.v1");
141
+ PROPAGATE_EVENT = "sibu:error-propagate";
142
+ }
143
+ });
144
+
51
145
  // src/utils/sanitize.ts
52
146
  function stripControlChars(value) {
53
147
  return value.replace(/[\x00-\x20\x7f-\x9f]+/g, "");
54
148
  }
149
+ function canonicalAttrName(name) {
150
+ return name.replace(/[A-Z]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) + 32));
151
+ }
55
152
  function isEventHandlerAttr(name) {
56
153
  if (name.length < 3) return false;
57
154
  const lower = name.toLowerCase();
58
155
  return lower[0] === "o" && lower[1] === "n" && lower.charCodeAt(2) >= 97 && lower.charCodeAt(2) <= 122;
59
156
  }
60
157
  function sanitizeUrl(url) {
61
- const trimmed = stripControlChars(url).trim();
62
- if (!trimmed) return "";
63
- const lower = trimmed.toLowerCase();
158
+ const probe = stripControlChars(url).trim();
159
+ if (!probe) return "";
160
+ const output2 = url.replace(OUTER_TRIM, "");
161
+ const lower = probe.toLowerCase();
64
162
  let schemeEnd = -1;
65
163
  for (let i2 = 0; i2 < lower.length; i2++) {
66
164
  const ch = lower.charCodeAt(i2);
@@ -70,11 +168,11 @@ function sanitizeUrl(url) {
70
168
  }
71
169
  if (ch === 47 || ch === 63 || ch === 35) break;
72
170
  }
73
- if (schemeEnd === -1) return trimmed;
171
+ if (schemeEnd === -1) return output2;
74
172
  const scheme = lower.slice(0, schemeEnd + 1);
75
- if (!/^[a-z][a-z0-9+.-]*:$/.test(scheme)) return trimmed;
173
+ if (!/^[a-z][a-z0-9+.-]*:$/.test(scheme)) return output2;
76
174
  if (SAFE_URL_PROTOCOLS.indexOf(scheme) === -1) return "";
77
- return trimmed;
175
+ return output2;
78
176
  }
79
177
  function sanitizeSrcset(value) {
80
178
  const parts = value.split(",");
@@ -84,43 +182,114 @@ function sanitizeSrcset(value) {
84
182
  if (!part) continue;
85
183
  const m = part.match(/^(\S+)(\s+.+)?$/);
86
184
  if (!m) continue;
185
+ if (m[2] !== void 0 && !SRCSET_DESCRIPTOR.test(m[2])) continue;
87
186
  const safe = sanitizeUrl(m[1]);
88
187
  if (!safe) continue;
89
188
  out.push(m[2] ? `${safe}${m[2]}` : safe);
90
189
  }
91
190
  return out.join(", ");
92
191
  }
192
+ function isCssWhitespace(ch) {
193
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f";
194
+ }
195
+ function isHexDigit(ch) {
196
+ return ch >= "0" && ch <= "9" || ch >= "a" && ch <= "f" || ch >= "A" && ch <= "F";
197
+ }
198
+ function decodeCssEscapes(value) {
199
+ let out = "";
200
+ for (let i2 = 0; i2 < value.length; i2++) {
201
+ const ch = value[i2];
202
+ if (ch !== "\\") {
203
+ out += ch;
204
+ continue;
205
+ }
206
+ if (i2 + 1 >= value.length) break;
207
+ const next = value[i2 + 1];
208
+ if (next === "\n" || next === "\f") {
209
+ i2 += 1;
210
+ continue;
211
+ }
212
+ if (next === "\r") {
213
+ i2 += value[i2 + 2] === "\n" ? 2 : 1;
214
+ continue;
215
+ }
216
+ if (isHexDigit(next)) {
217
+ let hex = "";
218
+ let j = i2 + 1;
219
+ while (j < value.length && hex.length < 6 && isHexDigit(value[j])) {
220
+ hex += value[j];
221
+ j++;
222
+ }
223
+ if (j < value.length && isCssWhitespace(value[j])) {
224
+ j += value[j] === "\r" && value[j + 1] === "\n" ? 2 : 1;
225
+ }
226
+ const code2 = Number.parseInt(hex, 16);
227
+ const invalid = code2 === 0 || code2 > 1114111 || code2 >= 55296 && code2 <= 57343;
228
+ out += invalid ? "\uFFFD" : String.fromCodePoint(code2);
229
+ i2 = j - 1;
230
+ continue;
231
+ }
232
+ out += next;
233
+ i2 += 1;
234
+ }
235
+ return out;
236
+ }
93
237
  function sanitizeCSSValue(value) {
94
238
  if (!CSS_DANGER_GATE.test(value)) return value;
95
- const decoded = value.replace(/\\([0-9a-fA-F]{1,6})\s?/g, (_m, hex) => {
96
- const code2 = Number.parseInt(hex, 16);
97
- if (!Number.isFinite(code2) || code2 < 0 || code2 > 1114111) return "";
98
- try {
99
- return String.fromCodePoint(code2);
100
- } catch {
101
- return "";
102
- }
103
- });
104
- const lower = decoded.toLowerCase().replace(/\s+/g, "");
239
+ const normalized = value.includes("\\") ? decodeCssEscapes(value) : value;
240
+ const lower = normalized.toLowerCase().replace(/\s+/g, "");
105
241
  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")) {
106
242
  return "";
107
243
  }
108
244
  return value;
109
245
  }
246
+ function sanitizeStyleAttribute(cssText) {
247
+ const input2 = String(cssText);
248
+ if (input2.trim() === "") return "";
249
+ if (typeof document === "undefined") {
250
+ return sanitizeCSSValue(input2) === "" ? "" : input2;
251
+ }
252
+ const probe = document.createElement("div");
253
+ try {
254
+ probe.style.cssText = input2;
255
+ } catch {
256
+ return "";
257
+ }
258
+ const declarations = [];
259
+ for (let i2 = 0; i2 < probe.style.length; i2++) {
260
+ const property = probe.style[i2];
261
+ const value = probe.style.getPropertyValue(property);
262
+ if (sanitizeCSSValue(value) === "") continue;
263
+ if (sanitizeCSSValue(`${property}:${value}`) === "") continue;
264
+ const priority = probe.style.getPropertyPriority(property);
265
+ declarations.push(`${property}: ${value}${priority ? ` !${priority}` : ""}`);
266
+ }
267
+ return declarations.join("; ");
268
+ }
269
+ function isHtmlContentAttribute(name) {
270
+ return HTML_CONTENT_ATTRIBUTES.has(name.toLowerCase());
271
+ }
110
272
  function isUrlAttribute(attr) {
111
273
  return URL_ATTRIBUTES.has(attr.toLowerCase());
112
274
  }
275
+ function isPolicyAttribute(name) {
276
+ const lower = name.toLowerCase();
277
+ return lower === "style" || URL_ATTRIBUTES.has(lower);
278
+ }
113
279
  function sanitizeAttributeString(attr, value) {
114
280
  const lower = attr.toLowerCase();
115
281
  if (lower === "srcset") return sanitizeSrcset(value);
116
282
  if (URL_ATTRIBUTES.has(lower)) return sanitizeUrl(value);
283
+ if (lower === "style") return sanitizeStyleAttribute(value);
117
284
  return value;
118
285
  }
119
- var SAFE_URL_PROTOCOLS, CSS_DANGER_GATE, URL_ATTRIBUTES;
286
+ var SAFE_URL_PROTOCOLS, OUTER_TRIM, SRCSET_DESCRIPTOR, CSS_DANGER_GATE, URL_ATTRIBUTES, HTML_CONTENT_ATTRIBUTES;
120
287
  var init_sanitize = __esm({
121
288
  "src/utils/sanitize.ts"() {
122
289
  "use strict";
123
290
  SAFE_URL_PROTOCOLS = ["http:", "https:", "mailto:", "tel:", "ftp:"];
291
+ OUTER_TRIM = /^[\x00-\x20\x7f-\x9f]+|[\x00-\x20\x7f-\x9f]+$/g;
292
+ SRCSET_DESCRIPTOR = /^\s+\d+(\.\d+)?[wx]$/;
124
293
  CSS_DANGER_GATE = /[(:@\\]/;
125
294
  URL_ATTRIBUTES = /* @__PURE__ */ new Set([
126
295
  "href",
@@ -134,8 +303,125 @@ var init_sanitize = __esm({
134
303
  "background",
135
304
  "srcset",
136
305
  "ping",
137
- "data"
306
+ "data",
307
+ // `<html manifest>` is a URL sink too. It was carried only by the SSR
308
+ // serializers' private copy of this set; folding that copy into this one would
309
+ // otherwise have silently dropped its coverage.
310
+ "manifest"
138
311
  ]);
312
+ HTML_CONTENT_ATTRIBUTES = /* @__PURE__ */ new Set(["srcdoc"]);
313
+ }
314
+ });
315
+
316
+ // src/core/rendering/dispose.ts
317
+ var dispose_exports = {};
318
+ __export(dispose_exports, {
319
+ MAX_DRAIN_TEARDOWNS: () => MAX_DRAIN_TEARDOWNS,
320
+ checkLeaks: () => checkLeaks,
321
+ dispose: () => dispose,
322
+ registerDisposer: () => registerDisposer,
323
+ replaceChildrenSafely: () => replaceChildrenSafely,
324
+ reportDrainRunaway: () => reportDrainRunaway,
325
+ unregisterDisposer: () => unregisterDisposer
326
+ });
327
+ function reportDrainRunaway(label2, executed, remaining) {
328
+ if (typeof console === "undefined") return;
329
+ console.error(
330
+ `[SibuJS ${label2}] runaway cleanup: stopped after running ${executed} teardowns with ${remaining} still queued. Cleanup production did not stabilize before the teardown safety ceiling was reached \u2014 the remaining work was NOT run.`,
331
+ { executed, remaining }
332
+ );
333
+ }
334
+ function registerDisposer(node, teardown) {
335
+ let disposers = elementDisposers.get(node);
336
+ if (!disposers) {
337
+ disposers = [];
338
+ elementDisposers.set(node, disposers);
339
+ }
340
+ disposers.push(teardown);
341
+ if (_isDev4) activeBindingCount++;
342
+ }
343
+ function unregisterDisposer(node, teardown) {
344
+ const disposers = elementDisposers.get(node);
345
+ if (!disposers) return;
346
+ const index = disposers.indexOf(teardown);
347
+ if (index === -1) return;
348
+ disposers.splice(index, 1);
349
+ if (_isDev4) activeBindingCount--;
350
+ if (disposers.length === 0) elementDisposers.delete(node);
351
+ }
352
+ function dispose(node) {
353
+ const stack = [node];
354
+ const order = [];
355
+ while (stack.length > 0) {
356
+ const current = stack.pop();
357
+ order.push(current);
358
+ const children = Array.from(current.childNodes);
359
+ for (let i2 = 0; i2 < children.length; i2++) {
360
+ stack.push(children[i2]);
361
+ }
362
+ }
363
+ for (let i2 = order.length - 1; i2 >= 0; i2--) {
364
+ const current = order[i2];
365
+ if (elementDisposers.has(current)) {
366
+ let executed = 0;
367
+ let runaway = false;
368
+ while (!runaway) {
369
+ const pending = elementDisposers.get(current);
370
+ if (!pending || pending.length === 0) break;
371
+ const snapshot = pending.slice();
372
+ elementDisposers.delete(current);
373
+ if (_isDev4) activeBindingCount -= snapshot.length;
374
+ for (let i3 = 0; i3 < snapshot.length; i3++) {
375
+ if (executed >= MAX_DRAIN_TEARDOWNS) {
376
+ const rest = snapshot.slice(i3);
377
+ const added = elementDisposers.get(current);
378
+ elementDisposers.set(current, added ? rest.concat(added) : rest);
379
+ if (_isDev4) activeBindingCount += rest.length;
380
+ reportDrainRunaway("dispose", executed, rest.length + (added?.length ?? 0));
381
+ runaway = true;
382
+ break;
383
+ }
384
+ executed++;
385
+ try {
386
+ snapshot[i3]();
387
+ } catch (err) {
388
+ reportError(err, { phase: "cleanup", name: "disposer" });
389
+ }
390
+ }
391
+ }
392
+ }
393
+ }
394
+ }
395
+ function replaceChildrenSafely(parent, ...next) {
396
+ for (let i2 = 0; i2 < next.length; i2++) {
397
+ const node = next[i2];
398
+ node.parentNode?.removeChild(node);
399
+ }
400
+ const current = Array.from(parent.childNodes);
401
+ for (let i2 = 0; i2 < current.length; i2++) {
402
+ dispose(current[i2]);
403
+ }
404
+ parent.replaceChildren(...next);
405
+ }
406
+ function checkLeaks(warnThreshold = 0) {
407
+ if (!_isDev4) return 0;
408
+ if (warnThreshold > 0 && activeBindingCount > warnThreshold) {
409
+ devWarn(
410
+ `checkLeaks: ${activeBindingCount} active DOM bindings detected. Expected \u2264${warnThreshold}. This may indicate a component was removed from the DOM without calling dispose().`
411
+ );
412
+ }
413
+ return activeBindingCount;
414
+ }
415
+ var elementDisposers, MAX_DRAIN_TEARDOWNS, _isDev4, activeBindingCount;
416
+ var init_dispose = __esm({
417
+ "src/core/rendering/dispose.ts"() {
418
+ "use strict";
419
+ init_dev();
420
+ init_errors();
421
+ elementDisposers = /* @__PURE__ */ new WeakMap();
422
+ MAX_DRAIN_TEARDOWNS = 1e4;
423
+ _isDev4 = isDev();
424
+ activeBindingCount = 0;
139
425
  }
140
426
  });
141
427
 
@@ -148,16 +434,19 @@ function detectSSRShared() {
148
434
  const getBuiltin = process.getBuiltinModule;
149
435
  if (typeof getBuiltin === "function") {
150
436
  mod = getBuiltin("node:async_hooks");
151
- } else {
152
- const req = Function("return typeof require==='function'?require:null")();
153
- if (req) mod = req("node:async_hooks");
437
+ } else if (typeof require === "function") {
438
+ mod = require("async_hooks");
154
439
  }
155
440
  if (mod) detected = new mod.AsyncLocalStorage();
156
441
  }
157
442
  } catch {
158
443
  detected = null;
159
444
  }
160
- return { als: detected, fallbackStore: { ssr: false, suspenseIdCounter: 0 } };
445
+ return {
446
+ als: detected,
447
+ fallbackStore: { ssr: false, suspenseIdCounter: 0, locale: void 0 },
448
+ fallbackDepth: 0
449
+ };
161
450
  }
162
451
  function getSSRStore() {
163
452
  if (als) {
@@ -166,6 +455,10 @@ function getSSRStore() {
166
455
  }
167
456
  return fallbackStore;
168
457
  }
458
+ function getRequestStore() {
459
+ if (als) return als.getStore() ?? null;
460
+ return _shared.fallbackDepth > 0 ? fallbackStore : null;
461
+ }
169
462
  function isSSR() {
170
463
  return getSSRStore().ssr;
171
464
  }
@@ -177,6 +470,179 @@ var init_ssr_context = __esm({
177
470
  _shared = globalThis[SSR_KEY] ?? (globalThis[SSR_KEY] = detectSSRShared());
178
471
  als = _shared.als;
179
472
  fallbackStore = _shared.fallbackStore;
473
+ _shared.fallbackDepth ?? (_shared.fallbackDepth = 0);
474
+ }
475
+ });
476
+
477
+ // src/utils/metaRefresh.ts
478
+ function isAsciiWhitespace(ch) {
479
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f";
480
+ }
481
+ function trimAsciiWhitespace(value) {
482
+ let start = 0;
483
+ let end = value.length;
484
+ while (start < end && isAsciiWhitespace(value[start])) start++;
485
+ while (end > start && isAsciiWhitespace(value[end - 1])) end--;
486
+ return value.slice(start, end);
487
+ }
488
+ function parseMetaRefreshContent(content) {
489
+ const raw = trimAsciiWhitespace(stripControlChars(content));
490
+ if (raw === "") return forbidden("empty content");
491
+ let i2 = 0;
492
+ while (i2 < raw.length && raw[i2] >= "0" && raw[i2] <= "9") i2++;
493
+ if (i2 === 0) return forbidden("missing or non-numeric delay");
494
+ const delay = Number.parseInt(raw.slice(0, i2), 10);
495
+ if (!Number.isFinite(delay)) return forbidden("unparseable delay");
496
+ while (i2 < raw.length && isAsciiWhitespace(raw[i2])) i2++;
497
+ if (i2 === raw.length) return { kind: "delay-only", delay };
498
+ if (raw[i2] !== ";" && raw[i2] !== ",") return forbidden("unexpected text after delay");
499
+ i2++;
500
+ while (i2 < raw.length && isAsciiWhitespace(raw[i2])) i2++;
501
+ const keyStart = i2;
502
+ while (i2 < raw.length && !isAsciiWhitespace(raw[i2]) && raw[i2] !== "=") i2++;
503
+ const key = raw.slice(keyStart, i2).toLowerCase();
504
+ if (key !== "url") return forbidden(`unsupported refresh key ${JSON.stringify(key)}`);
505
+ while (i2 < raw.length && isAsciiWhitespace(raw[i2])) i2++;
506
+ if (raw[i2] !== "=") return forbidden("missing '=' after url");
507
+ i2++;
508
+ while (i2 < raw.length && isAsciiWhitespace(raw[i2])) i2++;
509
+ let destination;
510
+ const quote = raw[i2];
511
+ if (quote === '"' || quote === "'") {
512
+ const close = raw.indexOf(quote, i2 + 1);
513
+ if (close === -1) return forbidden("unterminated quoted destination");
514
+ destination = raw.slice(i2 + 1, close);
515
+ const rest = trimAsciiWhitespace(raw.slice(close + 1));
516
+ if (rest !== "") return forbidden("trailing text after quoted destination");
517
+ } else {
518
+ destination = trimAsciiWhitespace(raw.slice(i2));
519
+ if (/[;,]/.test(destination)) return forbidden("multiple refresh assignments");
520
+ }
521
+ if (destination === "") return forbidden("empty destination");
522
+ const sanitizedDestination = sanitizeUrl(destination);
523
+ if (sanitizedDestination === "") return forbidden("destination uses a disallowed protocol");
524
+ return { kind: "allowed", delay, destination, sanitizedDestination };
525
+ }
526
+ function resolveMetaRefreshPolicy(attributes) {
527
+ let httpEquiv;
528
+ let content;
529
+ let duplicateName;
530
+ const seen = /* @__PURE__ */ new Set();
531
+ for (const [key, value] of attributes) {
532
+ const canonical = canonicalAttrName(key);
533
+ if (seen.has(canonical)) duplicateName = canonical;
534
+ seen.add(canonical);
535
+ if (canonical === "http-equiv") httpEquiv = value;
536
+ else if (canonical === "content") content = value;
537
+ }
538
+ if (duplicateName !== void 0) {
539
+ return forbidden(`duplicate case-insensitive attribute ${JSON.stringify(duplicateName)}`);
540
+ }
541
+ if (typeof httpEquiv !== "string") return { kind: "not-refresh" };
542
+ if (trimAsciiWhitespace(stripControlChars(httpEquiv)).toLowerCase() !== "refresh") {
543
+ return { kind: "not-refresh" };
544
+ }
545
+ if (typeof content !== "string") return { kind: "not-refresh" };
546
+ return parseMetaRefreshContent(content);
547
+ }
548
+ function normalizeMetaAttributes(props) {
549
+ const out = /* @__PURE__ */ new Map();
550
+ const seen = /* @__PURE__ */ new Set();
551
+ for (const key of Object.keys(props)) {
552
+ const canonical = canonicalAttrName(key);
553
+ if (seen.has(canonical)) return null;
554
+ seen.add(canonical);
555
+ out.set(key, props[key]);
556
+ }
557
+ return out;
558
+ }
559
+ function isForbiddenMetaEntry(props) {
560
+ const normalized = normalizeMetaAttributes(props);
561
+ if (normalized === null) return true;
562
+ return resolveMetaRefreshPolicy(normalized).kind === "forbidden";
563
+ }
564
+ var forbidden;
565
+ var init_metaRefresh = __esm({
566
+ "src/utils/metaRefresh.ts"() {
567
+ "use strict";
568
+ init_sanitize();
569
+ forbidden = (reason) => ({ kind: "forbidden", reason });
570
+ }
571
+ });
572
+
573
+ // src/utils/headEntry.ts
574
+ function isEmittableHeadAttrName(name) {
575
+ if (!SAFE_HEAD_ATTR_NAME.test(name)) return false;
576
+ const canonical = canonicalAttrName(name);
577
+ if (isEventHandlerAttr(canonical)) return false;
578
+ if (isHtmlContentAttribute(canonical)) return false;
579
+ return true;
580
+ }
581
+ function sanitizeHeadAttrValue(canonical, value) {
582
+ if (!isPolicyAttribute(canonical)) return value;
583
+ const safe = sanitizeAttributeString(canonical, value);
584
+ return safe === "" ? null : safe;
585
+ }
586
+ function findDuplicateAttributeName(names) {
587
+ const seen = /* @__PURE__ */ new Set();
588
+ for (const name of names) {
589
+ const canonical = canonicalAttrName(name);
590
+ if (seen.has(canonical)) return canonical;
591
+ seen.add(canonical);
592
+ }
593
+ return null;
594
+ }
595
+ function planEntry(props, resolver, applyRefreshPolicy) {
596
+ const rawNames = Object.keys(props);
597
+ const duplicate = findDuplicateAttributeName(rawNames);
598
+ if (duplicate !== null) {
599
+ return { kind: "drop", reason: `duplicate case-insensitive attribute ${JSON.stringify(duplicate)}` };
600
+ }
601
+ const attributes = /* @__PURE__ */ new Map();
602
+ let reactive = false;
603
+ for (const name of rawNames) {
604
+ if (!Object.hasOwn(props, name)) continue;
605
+ if (!isEmittableHeadAttrName(name)) continue;
606
+ const authored = props[name];
607
+ if (resolver.isReactiveValue?.(authored)) reactive = true;
608
+ const canonical = canonicalAttrName(name);
609
+ const sanitized = sanitizeHeadAttrValue(canonical, resolver.resolveValue(name, authored));
610
+ if (sanitized === null) continue;
611
+ attributes.set(canonical, sanitized);
612
+ }
613
+ if (attributes.size === 0) return { kind: "drop", reason: "no effective attributes remain" };
614
+ const refresh = applyRefreshPolicy ? resolveMetaRefreshPolicy(attributes) : { kind: "not-refresh" };
615
+ if (refresh.kind === "forbidden") return { kind: "drop", reason: refresh.reason };
616
+ if (reactive && (refresh.kind === "allowed" || refresh.kind === "delay-only")) {
617
+ return { kind: "drop", reason: "a reactive meta entry may not publish a native refresh directive" };
618
+ }
619
+ return { kind: "publish", attributes, refresh };
620
+ }
621
+ function planMetaEntry(props, resolver) {
622
+ return planEntry(props, resolver, true);
623
+ }
624
+ function planHeadElementEntry(props, resolver) {
625
+ return planEntry(props, resolver, false);
626
+ }
627
+ function escapeHeadAttrValue(value) {
628
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
629
+ }
630
+ function serializeHeadEntry(tag, attrs) {
631
+ const plan = tag === "meta" ? planMetaEntry(attrs, STATIC_VALUE_RESOLVER) : planHeadElementEntry(attrs, STATIC_VALUE_RESOLVER);
632
+ if (plan.kind === "drop") return "";
633
+ const pairs = Array.from(plan.attributes, ([name, value]) => `${name}="${escapeHeadAttrValue(value)}"`).join(" ");
634
+ return `<${tag} ${pairs} />`;
635
+ }
636
+ var SAFE_HEAD_ATTR_NAME, STATIC_VALUE_RESOLVER;
637
+ var init_headEntry = __esm({
638
+ "src/utils/headEntry.ts"() {
639
+ "use strict";
640
+ init_metaRefresh();
641
+ init_sanitize();
642
+ SAFE_HEAD_ATTR_NAME = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
643
+ STATIC_VALUE_RESOLVER = {
644
+ resolveValue: (_name, value) => String(value)
645
+ };
180
646
  }
181
647
  });
182
648
 
@@ -202,14 +668,20 @@ __export(ssr_exports, {
202
668
  suspenseSwapScript: () => suspenseSwapScript,
203
669
  trustHTML: () => trustHTML
204
670
  });
205
- function sanitizeUrlAttr(name, value) {
206
- return name === "srcset" ? sanitizeSrcset(value) : sanitizeUrl(value);
671
+ function isPolicyAttr(lowerName) {
672
+ return isPolicyAttribute(lowerName);
673
+ }
674
+ function isForbiddenSsrAttr(lowerName) {
675
+ return isHtmlContentAttribute(lowerName);
676
+ }
677
+ function sanitizeSsrAttributeValue(lowerName, value) {
678
+ return sanitizeAttributeString(lowerName, value);
207
679
  }
208
680
  function isSafeAttrName(name) {
209
681
  return SAFE_ATTR_NAME.test(name);
210
682
  }
211
683
  function ssrErrorComment(err) {
212
- if (_isDev9) {
684
+ if (_isDev8) {
213
685
  const msg = escapeHtml(err instanceof Error ? err.message : String(err));
214
686
  return `<!--SSR error: ${safeCommentText(msg)}-->`;
215
687
  }
@@ -239,20 +711,21 @@ function renderToString(element) {
239
711
  }
240
712
  const tag = element.tagName.toLowerCase();
241
713
  if (tag === "script" || tag === "style") {
242
- return _isDev9 ? `<!--ssr:${tag}-stripped-->` : "";
714
+ return _isDev8 ? `<!--ssr:${tag}-stripped-->` : "";
243
715
  }
244
716
  if (!/^[a-z][a-z0-9-]*$/i.test(tag)) {
245
- return _isDev9 ? "<!--ssr:invalid-tag-->" : "";
717
+ return _isDev8 ? "<!--ssr:invalid-tag-->" : "";
246
718
  }
247
719
  let html2 = `<${tag}`;
248
720
  for (const attr of Array.from(element.attributes)) {
249
721
  const rawName = attr.name;
250
722
  if (!isSafeAttrName(rawName)) continue;
251
723
  if (isEventHandlerAttr(rawName)) continue;
724
+ if (isForbiddenSsrAttr(rawName)) continue;
252
725
  const lowerName = rawName.toLowerCase();
253
726
  let value = attr.value;
254
- if (URL_ATTRS.has(lowerName)) {
255
- value = sanitizeUrlAttr(lowerName, value);
727
+ if (isPolicyAttr(lowerName)) {
728
+ value = sanitizeSsrAttributeValue(lowerName, value);
256
729
  if (!value) continue;
257
730
  }
258
731
  html2 += ` ${rawName}="${escapeAttr(value)}"`;
@@ -283,7 +756,7 @@ function hydrate(component, container, options = {}) {
283
756
  const first = mismatches[0];
284
757
  if (options.onMismatch) {
285
758
  options.onMismatch(first);
286
- } else if (_isDev9) {
759
+ } else if (_isDev8) {
287
760
  console.warn(
288
761
  `[SibuJS hydration] ${first.message}
289
762
  at ${first.path}
@@ -293,9 +766,20 @@ function hydrate(component, container, options = {}) {
293
766
  }
294
767
  }
295
768
  }
296
- container.replaceChildren(clientTree);
769
+ replaceChildrenSafely(container, clientTree);
297
770
  container.setAttribute("data-sibu-hydrated", "true");
298
771
  }
772
+ function directTextOf(node) {
773
+ let out = "";
774
+ for (const child of Array.from(node.childNodes)) {
775
+ if (child.nodeType === 3) out += child.nodeValue ?? "";
776
+ }
777
+ return out.replace(/\s+/g, " ").trim();
778
+ }
779
+ function disposeAndReplace(outgoing, next) {
780
+ dispose(outgoing);
781
+ outgoing.replaceWith(next);
782
+ }
299
783
  function collectMismatches(serverNode, clientNode, path2, out, max = 5) {
300
784
  if (out.length >= max) return;
301
785
  const nodePath = path2 || clientNode?.tagName?.toLowerCase() || "(root)";
@@ -371,6 +855,18 @@ function collectMismatches(serverNode, clientNode, path2, out, max = 5) {
371
855
  });
372
856
  }
373
857
  }
858
+ const serverText = directTextOf(serverNode);
859
+ const clientText = directTextOf(clientNode);
860
+ if (serverText !== clientText) {
861
+ out.push({
862
+ kind: "text",
863
+ path: nodePath,
864
+ serverValue: serverText,
865
+ clientValue: clientText,
866
+ message: "Text content differs between server and client."
867
+ });
868
+ if (out.length >= max) return;
869
+ }
374
870
  const serverChildren = Array.from(serverNode.children);
375
871
  const clientChildren = Array.from(clientNode.children);
376
872
  const max2 = Math.max(serverChildren.length, clientChildren.length);
@@ -390,10 +886,11 @@ function buildAttrString(attrs, { allowEventHandlers = false } = {}) {
390
886
  if (!Object.hasOwn(attrs, rawKey)) continue;
391
887
  if (!isSafeAttrName(rawKey)) continue;
392
888
  if (!allowEventHandlers && isEventHandlerAttr(rawKey)) continue;
889
+ if (isForbiddenSsrAttr(rawKey)) continue;
393
890
  const lowerKey = rawKey.toLowerCase();
394
891
  let value = String(attrs[rawKey]);
395
- if (URL_ATTRS.has(lowerKey)) {
396
- value = sanitizeUrlAttr(lowerKey, value);
892
+ if (isPolicyAttr(lowerKey)) {
893
+ value = sanitizeSsrAttributeValue(lowerKey, value);
397
894
  if (!value) continue;
398
895
  }
399
896
  out.push(`${rawKey}="${escapeAttr(value)}"`);
@@ -401,18 +898,7 @@ function buildAttrString(attrs, { allowEventHandlers = false } = {}) {
401
898
  return out.join(" ");
402
899
  }
403
900
  function isDangerousMetaRefresh(metaProps) {
404
- let httpEquiv;
405
- let content;
406
- for (const k in metaProps) {
407
- const lk = k.toLowerCase();
408
- if (lk === "http-equiv") httpEquiv = metaProps[k];
409
- else if (lk === "content") content = metaProps[k];
410
- }
411
- if (typeof httpEquiv !== "string") return false;
412
- if (httpEquiv.toLowerCase() !== "refresh") return false;
413
- if (typeof content !== "string") return false;
414
- const normalized = stripControlChars(content).toLowerCase();
415
- return normalized.includes("url=javascript:") || normalized.includes("url=data:") || normalized.includes("url=vbscript:") || normalized.includes("url=blob:");
901
+ return isForbiddenMetaEntry(metaProps);
416
902
  }
417
903
  function renderToDocument(component, options = {}) {
418
904
  let content;
@@ -421,15 +907,8 @@ function renderToDocument(component, options = {}) {
421
907
  } catch (err) {
422
908
  content = ssrErrorComment(err);
423
909
  }
424
- const metaTags = (options.meta || []).map((attrs) => {
425
- if (isDangerousMetaRefresh(attrs)) return "";
426
- const pairs = buildAttrString(attrs);
427
- return pairs ? `<meta ${pairs} />` : "";
428
- }).filter(Boolean).join("\n ");
429
- const linkTags = (options.links || []).map((attrs) => {
430
- const pairs = buildAttrString(attrs);
431
- return pairs ? `<link ${pairs} />` : "";
432
- }).filter(Boolean).join("\n ");
910
+ const metaTags = (options.meta || []).map((attrs) => serializeHeadEntry("meta", attrs)).filter(Boolean).join("\n ");
911
+ const linkTags = (options.links || []).map((attrs) => serializeHeadEntry("link", attrs)).filter(Boolean).join("\n ");
433
912
  const scriptTags = (options.scripts || []).map((src) => {
434
913
  const safe = sanitizeUrl(String(src));
435
914
  if (!safe) return "";
@@ -478,11 +957,11 @@ async function* renderToStream(element) {
478
957
  }
479
958
  const tag = element.tagName.toLowerCase();
480
959
  if (tag === "script" || tag === "style") {
481
- if (_isDev9) yield `<!--ssr:${tag}-stripped-->`;
960
+ if (_isDev8) yield `<!--ssr:${tag}-stripped-->`;
482
961
  return;
483
962
  }
484
963
  if (!/^[a-z][a-z0-9-]*$/i.test(tag)) {
485
- if (_isDev9) yield "<!--ssr:invalid-tag-->";
964
+ if (_isDev8) yield "<!--ssr:invalid-tag-->";
486
965
  return;
487
966
  }
488
967
  let openTag = `<${tag}`;
@@ -490,14 +969,18 @@ async function* renderToStream(element) {
490
969
  const rawName = attr.name;
491
970
  if (!isSafeAttrName(rawName)) continue;
492
971
  if (isEventHandlerAttr(rawName)) continue;
972
+ if (isForbiddenSsrAttr(rawName)) continue;
493
973
  const lowerName = rawName.toLowerCase();
494
974
  let value = attr.value;
495
- if (URL_ATTRS.has(lowerName)) {
496
- value = sanitizeUrlAttr(lowerName, value);
975
+ if (isPolicyAttr(lowerName)) {
976
+ value = sanitizeSsrAttributeValue(lowerName, value);
497
977
  if (!value) continue;
498
978
  }
499
979
  openTag += ` ${rawName}="${escapeAttr(value)}"`;
500
980
  }
981
+ if (element.dataset && !element.dataset.sibuHydrate) {
982
+ openTag += ` data-sibu-ssr="true"`;
983
+ }
501
984
  if (VOID_ELEMENTS.has(tag)) {
502
985
  yield `${openTag} />`;
503
986
  return;
@@ -543,9 +1026,13 @@ function island(id, component) {
543
1026
  el.setAttribute("data-sibu-island", id);
544
1027
  return el;
545
1028
  }
1029
+ function isAlreadyHydrated(element) {
1030
+ return element.hasAttribute("data-sibu-hydrated");
1031
+ }
546
1032
  function hydrateIslands(container, islands) {
547
1033
  const markers = container.querySelectorAll("[data-sibu-island]");
548
1034
  for (const marker2 of Array.from(markers)) {
1035
+ if (isAlreadyHydrated(marker2)) continue;
549
1036
  const id = marker2.getAttribute("data-sibu-island") ?? "";
550
1037
  if (!Object.hasOwn(islands, id)) continue;
551
1038
  const factory = islands[id];
@@ -553,7 +1040,7 @@ function hydrateIslands(container, islands) {
553
1040
  const clientTree = factory();
554
1041
  clientTree.setAttribute("data-sibu-island", id);
555
1042
  clientTree.setAttribute("data-sibu-hydrated", "true");
556
- marker2.replaceWith(clientTree);
1043
+ disposeAndReplace(marker2, clientTree);
557
1044
  }
558
1045
  container.setAttribute("data-sibu-hydrated", "partial");
559
1046
  }
@@ -561,6 +1048,7 @@ function hydrateProgressively(container, islands, options) {
561
1048
  const markers = container.querySelectorAll("[data-sibu-island]");
562
1049
  const cleanups = [];
563
1050
  for (const marker2 of Array.from(markers)) {
1051
+ if (isAlreadyHydrated(marker2)) continue;
564
1052
  const id = marker2.getAttribute("data-sibu-island") ?? "";
565
1053
  if (!Object.hasOwn(islands, id)) continue;
566
1054
  const factory = islands[id];
@@ -570,10 +1058,11 @@ function hydrateProgressively(container, islands, options) {
570
1058
  for (const entry of entries) {
571
1059
  if (entry.isIntersecting) {
572
1060
  observer.disconnect();
1061
+ if (isAlreadyHydrated(marker2) || marker2.parentNode === null) break;
573
1062
  const clientTree = factory();
574
1063
  clientTree.setAttribute("data-sibu-island", id);
575
1064
  clientTree.setAttribute("data-sibu-hydrated", "true");
576
- marker2.replaceWith(clientTree);
1065
+ disposeAndReplace(marker2, clientTree);
577
1066
  break;
578
1067
  }
579
1068
  }
@@ -614,7 +1103,7 @@ function ssrSuspense(props) {
614
1103
  },
615
1104
  (err) => {
616
1105
  if (timer) clearTimeout(timer);
617
- if (_isDev9) console.warn("[SibuJS SSR] ssrSuspense rejected:", err);
1106
+ if (_isDev8) console.warn("[SibuJS SSR] ssrSuspense rejected:", err);
618
1107
  return { id, html: fallbackHtml };
619
1108
  }
620
1109
  );
@@ -628,7 +1117,7 @@ function suspenseSwapScript(id, nonce) {
628
1117
  );
629
1118
  }
630
1119
  const nonceAttr = nonce ? ` nonce="${escapeAttr(nonce)}"` : "";
631
- return `<script${nonceAttr}>(function(){var t=document.getElementById("sibu-resolved-${id}");var f=document.querySelector('[data-sibu-suspense-id="${id}"]');if(t&&f){while(t.firstChild)f.appendChild(t.firstChild);t.remove();f.removeAttribute("data-sibu-suspense-id");}})()</script>`;
1120
+ return `<script${nonceAttr}>(function(){var t=document.getElementById("sibu-resolved-${id}");var f=document.querySelector('[data-sibu-suspense-id="${id}"]');if(t&&f){while(f.firstChild)f.removeChild(f.firstChild);while(t.firstChild)f.appendChild(t.firstChild);t.remove();f.removeAttribute("data-sibu-suspense-id");}})()</script>`;
632
1121
  }
633
1122
  async function* renderToSuspenseStream(element, pendingBoundaries = [], options) {
634
1123
  yield* renderToStream(element);
@@ -644,8 +1133,8 @@ async function* renderToSuspenseStream(element, pendingBoundaries = [], options)
644
1133
  function escapeScriptJson(json) {
645
1134
  return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
646
1135
  }
647
- function serializeState(state, nonce, options) {
648
- const rawJson = JSON.stringify(state);
1136
+ function serializeState(state2, nonce, options) {
1137
+ const rawJson = JSON.stringify(state2);
649
1138
  const maxBytes = options?.maxBytes ?? DEFAULT_MAX_SSR_BYTES;
650
1139
  const byteLen = typeof TextEncoder !== "undefined" ? new TextEncoder().encode(rawJson).byteLength : Buffer.byteLength(rawJson, "utf8");
651
1140
  if (byteLen > maxBytes) {
@@ -657,7 +1146,7 @@ function serializeState(state, nonce, options) {
657
1146
  }
658
1147
  function deserializeState(validate) {
659
1148
  if (typeof window === "undefined") return void 0;
660
- if (_isDev9 && !validate) {
1149
+ if (_isDev8 && !validate) {
661
1150
  console.warn(
662
1151
  "[SibuJS SSR] deserializeState() called without a validate guard \u2014 tampered SSR payloads will not be detected."
663
1152
  );
@@ -674,29 +1163,18 @@ function escapeHtml(str) {
674
1163
  function escapeAttr(str) {
675
1164
  return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
676
1165
  }
677
- var _isDev9, SAFE_ATTR_NAME, URL_ATTRS, VOID_ELEMENTS, SAFE_ID, SAFE_SUSPENSE_ID, SSR_DATA_ATTR, DEFAULT_MAX_SSR_BYTES;
1166
+ var _isDev8, SAFE_ATTR_NAME, VOID_ELEMENTS, SAFE_ID, SAFE_SUSPENSE_ID, SSR_DATA_ATTR, DEFAULT_MAX_SSR_BYTES;
678
1167
  var init_ssr = __esm({
679
1168
  "src/platform/ssr.ts"() {
680
1169
  "use strict";
681
1170
  init_dev();
1171
+ init_dispose();
682
1172
  init_ssr_context();
1173
+ init_headEntry();
1174
+ init_metaRefresh();
683
1175
  init_sanitize();
684
- _isDev9 = isDev();
1176
+ _isDev8 = isDev();
685
1177
  SAFE_ATTR_NAME = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
686
- URL_ATTRS = /* @__PURE__ */ new Set([
687
- "href",
688
- "src",
689
- "action",
690
- "formaction",
691
- "cite",
692
- "poster",
693
- "background",
694
- "srcset",
695
- "ping",
696
- "manifest",
697
- "data",
698
- "xlink:href"
699
- ]);
700
1178
  VOID_ELEMENTS = /* @__PURE__ */ new Set([
701
1179
  "area",
702
1180
  "base",
@@ -731,6 +1209,7 @@ __export(plugins_exports, {
731
1209
  Suspense: () => Suspense,
732
1210
  Trans: () => Trans,
733
1211
  VERSION: () => VERSION,
1212
+ __getNavigationEpoch: () => __getNavigationEpoch,
734
1213
  __removeRouterPagehideHandler: () => __removeRouterPagehideHandler,
735
1214
  addRoute: () => addRoute,
736
1215
  afterEach: () => afterEach,
@@ -805,15 +1284,92 @@ module.exports = __toCommonJS(plugins_exports);
805
1284
  init_dev();
806
1285
 
807
1286
  // src/reactivity/bindAttribute.ts
1287
+ init_errors();
1288
+ init_sanitize();
1289
+
1290
+ // src/utils/setSafeAttribute.ts
808
1291
  init_dev();
809
1292
  init_sanitize();
1293
+ var _isDev2 = isDev();
1294
+ var HTML_NS = "http://www.w3.org/1999/xhtml";
1295
+ var SVG_NS = "http://www.w3.org/2000/svg";
1296
+ var XLINK_NS = "http://www.w3.org/1999/xlink";
1297
+ var XML_NS = "http://www.w3.org/XML/1998/namespace";
1298
+ var BOOLEAN_IDL_ATTRS = /* @__PURE__ */ new Set(["checked", "disabled", "selected"]);
1299
+ function isHtmlElement(el) {
1300
+ return el.namespaceURI === null || el.namespaceURI === HTML_NS;
1301
+ }
1302
+ function setProp(el, key, val) {
1303
+ el[key] = val;
1304
+ }
1305
+ function namespaceFor(el, name) {
1306
+ if (el.namespaceURI !== SVG_NS) return null;
1307
+ const lower = name.toLowerCase();
1308
+ if (lower.startsWith("xlink:")) return XLINK_NS;
1309
+ if (lower.startsWith("xml:")) return XML_NS;
1310
+ return null;
1311
+ }
1312
+ function setSafeAttribute(el, name, value, options = {}) {
1313
+ const ns = namespaceFor(el, name);
1314
+ const localName = ns ? name.slice(name.indexOf(":") + 1) : name;
1315
+ if (isEventHandlerAttr(name)) {
1316
+ if (_isDev2) {
1317
+ devWarn(
1318
+ `${options.label ?? "setSafeAttribute"}: refusing to set event-handler attribute "${name}". Its value would be evaluated as JavaScript. Use on:{ ${name.slice(2)}: fn } instead.`
1319
+ );
1320
+ }
1321
+ if (ns) el.removeAttributeNS(ns, localName);
1322
+ else el.removeAttribute(name);
1323
+ return false;
1324
+ }
1325
+ if (value == null) {
1326
+ if (ns) el.removeAttributeNS(ns, localName);
1327
+ else el.removeAttribute(name);
1328
+ return true;
1329
+ }
1330
+ if (isHtmlContentAttribute(name)) {
1331
+ if (_isDev2) {
1332
+ devWarn(
1333
+ `${options.label ?? "setSafeAttribute"}: refusing to set "${name}". The browser parses this attribute as a nested HTML document, so a generic string value cannot be made safe \u2014 attribute escaping is undone before the parse. Build the frame's content another way.`
1334
+ );
1335
+ }
1336
+ if (ns) el.removeAttributeNS(ns, localName);
1337
+ else el.removeAttribute(name);
1338
+ return false;
1339
+ }
1340
+ const html2 = isHtmlElement(el);
1341
+ const idlName = html2 ? name.toLowerCase() : name;
1342
+ if (typeof value === "boolean") {
1343
+ if (html2 && BOOLEAN_IDL_ATTRS.has(idlName) && idlName in el) {
1344
+ setProp(el, idlName, value);
1345
+ } else if (value) {
1346
+ if (ns) el.setAttributeNS(ns, name, "");
1347
+ else el.setAttribute(name, "");
1348
+ } else if (ns) {
1349
+ el.removeAttributeNS(ns, localName);
1350
+ } else {
1351
+ el.removeAttribute(name);
1352
+ }
1353
+ return true;
1354
+ }
1355
+ const str = String(value);
1356
+ if (options.syncValueProperty !== false && html2 && (idlName === "value" || idlName === "checked") && idlName in el) {
1357
+ setProp(el, idlName, idlName === "checked" ? Boolean(value) : str);
1358
+ return true;
1359
+ }
1360
+ const safe = sanitizeAttributeString(name, str);
1361
+ const current = ns ? el.getAttributeNS(ns, localName) : el.getAttribute(name);
1362
+ if (current === safe) return true;
1363
+ if (ns) el.setAttributeNS(ns, name, safe);
1364
+ else el.setAttribute(name, safe);
1365
+ return true;
1366
+ }
810
1367
 
811
1368
  // src/reactivity/track.ts
812
1369
  init_dev();
813
1370
 
814
1371
  // src/reactivity/track-core.ts
815
- init_dev();
816
- var _isDev2 = isDev();
1372
+ init_errors();
817
1373
  var POOL_MAX = 4096;
818
1374
  var nodePool = [];
819
1375
  function createNode() {
@@ -821,6 +1377,7 @@ function createNode() {
821
1377
  sig: null,
822
1378
  sub: null,
823
1379
  epoch: 0,
1380
+ depVersion: 0,
824
1381
  sigPrev: null,
825
1382
  sigNext: null,
826
1383
  subPrev: null,
@@ -905,9 +1462,28 @@ function safeInvoke(sub2) {
905
1462
  try {
906
1463
  sub2();
907
1464
  } catch (err) {
908
- if (_isDev2) devWarn(`Subscriber threw during notification: ${err instanceof Error ? err.message : String(err)}`);
1465
+ const s2 = sub2;
1466
+ reportError(err, { phase: s2._errorPhase ?? "effect", name: s2.__name, node: s2._errorNode });
909
1467
  }
910
1468
  }
1469
+ function depsChanged(sub2) {
1470
+ let node = sub2.depsHead ?? null;
1471
+ if (node === null) return true;
1472
+ while (node !== null) {
1473
+ const sig = node.sig;
1474
+ if (sig === null) return true;
1475
+ if (sig._d === true && sig._validate !== void 0) {
1476
+ try {
1477
+ sig._validate();
1478
+ } catch {
1479
+ return true;
1480
+ }
1481
+ }
1482
+ if (node.depVersion !== sig.__v) return true;
1483
+ node = node.subNext;
1484
+ }
1485
+ return false;
1486
+ }
911
1487
  var suspendDepth = 0;
912
1488
  var trackingSuspended = false;
913
1489
  function suspendTracking() {
@@ -988,7 +1564,7 @@ function track(effectFn, subscriber) {
988
1564
  const sub2 = subscriber;
989
1565
  return sub2._dispose ?? (sub2._dispose = () => cleanup(subscriber));
990
1566
  }
991
- function reactiveBinding(commit) {
1567
+ function reactiveBinding(commit, ownerNode) {
992
1568
  const run = () => {
993
1569
  const s2 = subscriber;
994
1570
  if (s2._disposed || s2._reentrant) return;
@@ -1004,13 +1580,17 @@ function reactiveBinding(commit) {
1004
1580
  subscriber.depsTail = null;
1005
1581
  subscriber._epoch = 0;
1006
1582
  subscriber._structDirty = false;
1583
+ subscriber._hasComputedDep = false;
1007
1584
  subscriber._runEpoch = 0;
1008
1585
  subscriber._runs = 0;
1009
1586
  subscriber._reentrant = false;
1010
1587
  subscriber._disposed = false;
1588
+ subscriber._errorPhase = "binding";
1589
+ subscriber._errorNode = ownerNode;
1011
1590
  run();
1012
1591
  return subscriber._dispose ?? (subscriber._dispose = () => {
1013
1592
  subscriber._disposed = true;
1593
+ subscriber._errorNode = void 0;
1014
1594
  cleanup(subscriber);
1015
1595
  });
1016
1596
  }
@@ -1022,9 +1602,12 @@ function recordDependency(signal2) {
1022
1602
  const active = sig.__activeNode ?? null;
1023
1603
  if (active !== null && active.sub === sub2) {
1024
1604
  active.epoch = epoch;
1605
+ active.depVersion = sig.__v ?? Number.NaN;
1025
1606
  return;
1026
1607
  }
1608
+ if (sig._validate !== void 0) sub2._hasComputedDep = true;
1027
1609
  const node = allocNode(signal2, sub2, epoch);
1610
+ node.depVersion = sig.__v ?? Number.NaN;
1028
1611
  node.prevActive = active;
1029
1612
  sig.__activeNode = node;
1030
1613
  linkSub(sub2, node);
@@ -1043,9 +1626,10 @@ function cleanup(subscriber) {
1043
1626
  node = next;
1044
1627
  }
1045
1628
  }
1046
- var maxSubscriberRepeats = 50;
1629
+ var maxSubscriberRepeats = 1e3;
1047
1630
  var maxDrainIterations = 1e6;
1048
1631
  var drainEpoch = 0;
1632
+ var quarantined = /* @__PURE__ */ new Set();
1049
1633
  function setMaxSubscriberRepeats(n) {
1050
1634
  const prev = maxSubscriberRepeats;
1051
1635
  if (Number.isFinite(n) && n > 0) maxSubscriberRepeats = Math.floor(n);
@@ -1067,19 +1651,21 @@ function tickRepeat(sub2) {
1067
1651
  return s2._runs > maxSubscriberRepeats;
1068
1652
  }
1069
1653
  function cycleError(sub2) {
1070
- if (typeof console !== "undefined") {
1071
- const name = sub2.__name ?? "<unnamed>";
1072
- console.error(
1073
- `[SibuJS] subscriber "${name}" fired more than ${maxSubscriberRepeats} times \u2014 likely a write-reads-self cycle between effects/signals. Breaking to prevent infinite loop.`
1074
- );
1075
- }
1654
+ const name = sub2.__name ?? "<unnamed>";
1655
+ reportError(
1656
+ new Error(
1657
+ `subscriber "${name}" fired more than ${maxSubscriberRepeats} times \u2014 likely a write-reads-self cycle between effects/signals. This subscriber is quarantined for the rest of this update; other pending work still runs.`
1658
+ ),
1659
+ { phase: "scheduler", name }
1660
+ );
1076
1661
  }
1077
1662
  function absoluteDrainError() {
1078
- if (typeof console !== "undefined") {
1079
- console.error(
1080
- `[SibuJS] Notification drain exceeded ${maxDrainIterations} iterations \u2014 absolute safety net tripped. Breaking to prevent infinite loop.`
1081
- );
1082
- }
1663
+ reportError(
1664
+ new Error(
1665
+ `Notification drain exceeded ${maxDrainIterations} iterations \u2014 absolute safety net tripped. Breaking to prevent infinite loop.`
1666
+ ),
1667
+ { phase: "scheduler" }
1668
+ );
1083
1669
  }
1084
1670
  function drainQueue() {
1085
1671
  let i2 = 0;
@@ -1089,11 +1675,14 @@ function drainQueue() {
1089
1675
  break;
1090
1676
  }
1091
1677
  const sub2 = pendingQueue[i2++];
1678
+ pendingSet.delete(sub2);
1679
+ if (quarantined.size > 0 && quarantined.has(sub2)) continue;
1680
+ if (sub2._hasComputedDep === true && !depsChanged(sub2)) continue;
1092
1681
  if (tickRepeat(sub2)) {
1093
1682
  cycleError(sub2);
1094
- break;
1683
+ quarantined.add(sub2);
1684
+ continue;
1095
1685
  }
1096
- pendingSet.delete(sub2);
1097
1686
  safeInvoke(sub2);
1098
1687
  }
1099
1688
  }
@@ -1108,6 +1697,7 @@ function drainNotificationQueue() {
1108
1697
  if (notifyDepth === 0) {
1109
1698
  pendingQueue.length = 0;
1110
1699
  pendingSet.clear();
1700
+ if (quarantined.size > 0) quarantined.clear();
1111
1701
  }
1112
1702
  }
1113
1703
  }
@@ -1201,6 +1791,7 @@ function notifySubscribers(signal2) {
1201
1791
  if (notifyDepth === 0) {
1202
1792
  pendingQueue.length = 0;
1203
1793
  pendingSet.clear();
1794
+ if (quarantined.size > 0) quarantined.clear();
1204
1795
  }
1205
1796
  }
1206
1797
  }
@@ -1228,7 +1819,7 @@ function forEachSubscriber(signal2, visit) {
1228
1819
 
1229
1820
  // src/reactivity/track.ts
1230
1821
  var _isDev3 = isDev();
1231
- var _runtimeVersion = true ? "3.4.1" : "dev";
1822
+ var _runtimeVersion = true ? "4.0.0" : "dev";
1232
1823
  var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("sibujs.reactive.v1");
1233
1824
  function resolveReactiveApi() {
1234
1825
  const g2 = globalThis;
@@ -1285,16 +1876,9 @@ var getSubscriberDeps2 = API.getSubscriberDeps;
1285
1876
  var forEachSubscriber2 = API.forEachSubscriber;
1286
1877
 
1287
1878
  // src/reactivity/bindAttribute.ts
1288
- var _isDev4 = isDev();
1289
- function setProp(el, key, val) {
1290
- el[key] = val;
1291
- }
1292
1879
  function bindAttribute(el, attr, getter) {
1293
1880
  if (isEventHandlerAttr(attr)) {
1294
- if (_isDev4)
1295
- devWarn(
1296
- `bindAttribute: refusing to bind event-handler attribute "${attr}". Use on:{ ${attr.slice(2)}: fn } instead.`
1297
- );
1881
+ setSafeAttribute(el, attr, "", { label: `bindAttribute("${attr}")` });
1298
1882
  return () => {
1299
1883
  };
1300
1884
  }
@@ -1303,97 +1887,19 @@ function bindAttribute(el, attr, getter) {
1303
1887
  try {
1304
1888
  value = getter();
1305
1889
  } catch (err) {
1306
- if (_isDev4)
1307
- devWarn(`bindAttribute: getter for "${attr}" threw: ${err instanceof Error ? err.message : String(err)}`);
1308
- return;
1309
- }
1310
- if (typeof value === "boolean") {
1311
- if (attr in el && (attr === "checked" || attr === "disabled" || attr === "selected")) {
1312
- setProp(el, attr, value);
1313
- } else if (value) {
1314
- el.setAttribute(attr, "");
1315
- } else {
1316
- el.removeAttribute(attr);
1317
- }
1890
+ reportError(err, { phase: "binding", name: `bindAttribute("${attr}")`, node: el });
1318
1891
  return;
1319
1892
  }
1320
- const str = String(value);
1321
- if ((attr === "value" || attr === "checked") && attr in el) {
1322
- setProp(el, attr, attr === "checked" ? Boolean(value) : str);
1323
- } else {
1324
- el.setAttribute(attr, sanitizeAttributeString(attr, str));
1325
- }
1893
+ setSafeAttribute(el, attr, value, { label: `bindAttribute("${attr}")` });
1326
1894
  }
1327
- return reactiveBinding2(commit);
1895
+ return reactiveBinding2(commit, el);
1328
1896
  }
1329
1897
 
1330
1898
  // src/reactivity/bindChildNode.ts
1331
1899
  init_dev();
1332
-
1333
- // src/core/rendering/dispose.ts
1334
- init_dev();
1335
- var elementDisposers = /* @__PURE__ */ new WeakMap();
1900
+ init_errors();
1901
+ init_dispose();
1336
1902
  var _isDev5 = isDev();
1337
- var activeBindingCount = 0;
1338
- function registerDisposer(node, teardown) {
1339
- let disposers = elementDisposers.get(node);
1340
- if (!disposers) {
1341
- disposers = [];
1342
- elementDisposers.set(node, disposers);
1343
- }
1344
- disposers.push(teardown);
1345
- if (_isDev5) activeBindingCount++;
1346
- }
1347
- function dispose(node) {
1348
- const stack = [node];
1349
- const order = [];
1350
- while (stack.length > 0) {
1351
- const current = stack.pop();
1352
- order.push(current);
1353
- const children = Array.from(current.childNodes);
1354
- for (let i2 = 0; i2 < children.length; i2++) {
1355
- stack.push(children[i2]);
1356
- }
1357
- }
1358
- for (let i2 = order.length - 1; i2 >= 0; i2--) {
1359
- const current = order[i2];
1360
- const disposers = elementDisposers.get(current);
1361
- if (disposers) {
1362
- const snapshot = disposers.slice();
1363
- elementDisposers.delete(current);
1364
- if (_isDev5) activeBindingCount -= snapshot.length;
1365
- for (const d of snapshot) {
1366
- try {
1367
- d();
1368
- } catch (err) {
1369
- if (_isDev5 && typeof console !== "undefined") {
1370
- console.warn("[SibuJS] Disposer threw during cleanup:", err);
1371
- }
1372
- }
1373
- }
1374
- let extraPasses = 0;
1375
- while (extraPasses++ < 8) {
1376
- const added = elementDisposers.get(current);
1377
- if (!added || added.length === 0) break;
1378
- const moreSnapshot = added.slice();
1379
- elementDisposers.delete(current);
1380
- if (_isDev5) activeBindingCount -= moreSnapshot.length;
1381
- for (const d of moreSnapshot) {
1382
- try {
1383
- d();
1384
- } catch (err) {
1385
- if (_isDev5 && typeof console !== "undefined") {
1386
- console.warn("[SibuJS] Disposer threw during cleanup:", err);
1387
- }
1388
- }
1389
- }
1390
- }
1391
- }
1392
- }
1393
- }
1394
-
1395
- // src/reactivity/bindChildNode.ts
1396
- var _isDev6 = isDev();
1397
1903
  function bindChildNode(placeholder, getter) {
1398
1904
  let lastNodes = [];
1399
1905
  function commit() {
@@ -1401,7 +1907,7 @@ function bindChildNode(placeholder, getter) {
1401
1907
  try {
1402
1908
  result = getter();
1403
1909
  } catch (err) {
1404
- if (_isDev6) devWarn(`bindChildNode: getter threw: ${err instanceof Error ? err.message : String(err)}`);
1910
+ reportError(err, { phase: "binding", name: "bindChildNode", node: placeholder });
1405
1911
  return;
1406
1912
  }
1407
1913
  if (result == null || typeof result === "boolean") {
@@ -1427,7 +1933,7 @@ function bindChildNode(placeholder, getter) {
1427
1933
  if (item == null || typeof item === "boolean") continue;
1428
1934
  const node = item instanceof Node ? item : document.createTextNode(String(item));
1429
1935
  if (seen.has(node)) {
1430
- if (_isDev6)
1936
+ if (_isDev5)
1431
1937
  devWarn("bindChildNode: duplicate node reference in array \u2014 only the first occurrence is rendered.");
1432
1938
  continue;
1433
1939
  }
@@ -1462,13 +1968,14 @@ function bindChildNode(placeholder, getter) {
1462
1968
  }
1463
1969
  lastNodes = newNodes;
1464
1970
  }
1465
- return reactiveBinding2(commit);
1971
+ return reactiveBinding2(commit, placeholder);
1466
1972
  }
1467
1973
 
1468
1974
  // src/core/rendering/tagFactory.ts
1469
1975
  init_sanitize();
1470
- var SVG_NS = "http://www.w3.org/2000/svg";
1471
- var _isDev7 = isDev();
1976
+ init_dispose();
1977
+ var SVG_NS2 = "http://www.w3.org/2000/svg";
1978
+ var _isDev6 = isDev();
1472
1979
  var BLOCKED_TAGS = /* @__PURE__ */ new Set(["script", "iframe", "object", "embed", "frame", "frameset"]);
1473
1980
  function isBlockedTag(tag) {
1474
1981
  return BLOCKED_TAGS.has(tag.toLowerCase());
@@ -1486,9 +1993,6 @@ var CLOBBER_RISKY_IDS = /* @__PURE__ */ new Set([
1486
1993
  "self",
1487
1994
  "frames"
1488
1995
  ]);
1489
- function setProp2(el, key, val) {
1490
- el[key] = val;
1491
- }
1492
1996
  function looksLikeClassList(s2) {
1493
1997
  const t2 = s2.trim();
1494
1998
  if (!t2) return false;
@@ -1512,13 +2016,13 @@ function toKebab(prop) {
1512
2016
  function applyStyle(el, style2) {
1513
2017
  if (typeof style2 === "function") {
1514
2018
  const teardown = track2(() => {
1515
- el.setAttribute("style", style2());
2019
+ el.setAttribute("style", sanitizeStyleAttribute(String(style2())));
1516
2020
  });
1517
2021
  registerDisposer(el, teardown);
1518
2022
  return;
1519
2023
  }
1520
2024
  if (typeof style2 === "string") {
1521
- el.setAttribute("style", style2);
2025
+ el.setAttribute("style", sanitizeStyleAttribute(style2));
1522
2026
  return;
1523
2027
  }
1524
2028
  const htmlEl = el;
@@ -1639,7 +2143,7 @@ var tagFactory = (tag, ns) => {
1639
2143
  appendChildren(el, second);
1640
2144
  return el;
1641
2145
  }
1642
- if (_isDev7 && looksLikeClassList(first)) {
2146
+ if (_isDev6 && looksLikeClassList(first)) {
1643
2147
  devWarn(
1644
2148
  `tagFactory: lone string "${first}" looks like a class list but is being rendered as TEXT. For a class, use ${tag}({ class: "${first}" }) \u2014 or ${tag}("${first}", children) to set the class AND add children.`
1645
2149
  );
@@ -1660,7 +2164,7 @@ var tagFactory = (tag, ns) => {
1660
2164
  if (pClass != null) applyClass(el, pClass);
1661
2165
  const pId = props.id;
1662
2166
  if (pId != null) {
1663
- if (_isDev7 && typeof pId === "string" && CLOBBER_RISKY_IDS.has(pId.toLowerCase())) {
2167
+ if (_isDev6 && typeof pId === "string" && CLOBBER_RISKY_IDS.has(pId.toLowerCase())) {
1664
2168
  devWarn(
1665
2169
  `tagFactory: element id="${pId}" matches a common global and may cause DOM clobbering. Avoid setting ids from untrusted input.`
1666
2170
  );
@@ -1675,7 +2179,7 @@ var tagFactory = (tag, ns) => {
1675
2179
  const handler = pOn[ev];
1676
2180
  if (typeof handler === "function") {
1677
2181
  el.addEventListener(ev, handler);
1678
- } else if (_isDev7) {
2182
+ } else if (_isDev6) {
1679
2183
  devWarn(
1680
2184
  `tagFactory: on.${ev} handler is not a function (got ${typeof handler}). Event listener was not attached.`
1681
2185
  );
@@ -1703,16 +2207,8 @@ var tagFactory = (tag, ns) => {
1703
2207
  if (isEventHandlerAttr(key)) continue;
1704
2208
  if (typeof value === "function") {
1705
2209
  registerDisposer(el, bindAttribute(el, key, value));
1706
- } else if (typeof value === "boolean") {
1707
- if (key in el && (key === "checked" || key === "disabled" || key === "selected")) {
1708
- setProp2(el, key, value);
1709
- } else if (value) {
1710
- el.setAttribute(key, "");
1711
- } else {
1712
- el.removeAttribute(key);
1713
- }
1714
2210
  } else {
1715
- el.setAttribute(key, sanitizeAttributeString(key, String(value)));
2211
+ setSafeAttribute(el, key, value, { syncValueProperty: false, label: "tagFactory" });
1716
2212
  }
1717
2213
  }
1718
2214
  }
@@ -1797,7 +2293,7 @@ var param = tagFactory("param");
1797
2293
  var picture = tagFactory("picture");
1798
2294
  var portal = tagFactory("portal");
1799
2295
  var source = tagFactory("source");
1800
- var svg = tagFactory("svg", SVG_NS);
2296
+ var svg = tagFactory("svg", SVG_NS2);
1801
2297
  var math = tagFactory("math");
1802
2298
  var canvas = tagFactory("canvas");
1803
2299
  var noscript = tagFactory("noscript");
@@ -1838,26 +2334,26 @@ var base = tagFactory("base");
1838
2334
  var link = tagFactory("link");
1839
2335
  var meta = tagFactory("meta");
1840
2336
  var style = tagFactory("style");
1841
- var circle = tagFactory("circle", SVG_NS);
1842
- var ellipse = tagFactory("ellipse", SVG_NS);
1843
- var g = tagFactory("g", SVG_NS);
1844
- var line = tagFactory("line", SVG_NS);
1845
- var path = tagFactory("path", SVG_NS);
1846
- var polygon = tagFactory("polygon", SVG_NS);
1847
- var polyline = tagFactory("polyline", SVG_NS);
1848
- var rect = tagFactory("rect", SVG_NS);
1849
- var text = tagFactory("text", SVG_NS);
1850
- var tspan = tagFactory("tspan", SVG_NS);
1851
- var defs = tagFactory("defs", SVG_NS);
1852
- var clipPath = tagFactory("clipPath", SVG_NS);
1853
- var mask = tagFactory("mask", SVG_NS);
1854
- var pattern = tagFactory("pattern", SVG_NS);
1855
- var linearGradient = tagFactory("linearGradient", SVG_NS);
1856
- var radialGradient = tagFactory("radialGradient", SVG_NS);
1857
- var stop = tagFactory("stop", SVG_NS);
1858
- var use = tagFactory("use", SVG_NS);
1859
- var symbol = tagFactory("symbol", SVG_NS);
1860
- var marker = tagFactory("marker", SVG_NS);
2337
+ var circle = tagFactory("circle", SVG_NS2);
2338
+ var ellipse = tagFactory("ellipse", SVG_NS2);
2339
+ var g = tagFactory("g", SVG_NS2);
2340
+ var line = tagFactory("line", SVG_NS2);
2341
+ var path = tagFactory("path", SVG_NS2);
2342
+ var polygon = tagFactory("polygon", SVG_NS2);
2343
+ var polyline = tagFactory("polyline", SVG_NS2);
2344
+ var rect = tagFactory("rect", SVG_NS2);
2345
+ var text = tagFactory("text", SVG_NS2);
2346
+ var tspan = tagFactory("tspan", SVG_NS2);
2347
+ var defs = tagFactory("defs", SVG_NS2);
2348
+ var clipPath = tagFactory("clipPath", SVG_NS2);
2349
+ var mask = tagFactory("mask", SVG_NS2);
2350
+ var pattern = tagFactory("pattern", SVG_NS2);
2351
+ var linearGradient = tagFactory("linearGradient", SVG_NS2);
2352
+ var radialGradient = tagFactory("radialGradient", SVG_NS2);
2353
+ var stop = tagFactory("stop", SVG_NS2);
2354
+ var use = tagFactory("use", SVG_NS2);
2355
+ var symbol = tagFactory("symbol", SVG_NS2);
2356
+ var marker = tagFactory("marker", SVG_NS2);
1861
2357
  var center = tagFactory("center");
1862
2358
  var font = tagFactory("font");
1863
2359
  var marquee = tagFactory("marquee");
@@ -1915,74 +2411,79 @@ var isBatching = API2.isBatching;
1915
2411
  // src/core/signals/signal.ts
1916
2412
  init_dev();
1917
2413
  var _g = globalThis;
1918
- var _isDev8 = isDev();
2414
+ var _isDev7 = isDev();
1919
2415
  function signal(initial, options) {
1920
- const state = {
2416
+ const state2 = {
1921
2417
  value: initial,
1922
2418
  __v: 0,
1923
2419
  __sc: 0,
1924
2420
  subsHead: null,
1925
2421
  subsTail: null,
1926
2422
  __activeNode: null,
1927
- __name: void 0
2423
+ __name: void 0,
2424
+ _d: false,
2425
+ _validate: void 0
1928
2426
  };
1929
- const debugName = _isDev8 ? options?.name : void 0;
2427
+ const debugName = _isDev7 ? options?.name : void 0;
1930
2428
  const equalsFn = options?.equals;
1931
- if (debugName) state.__name = debugName;
2429
+ if (debugName) state2.__name = debugName;
1932
2430
  function get() {
1933
- recordDependency2(state);
1934
- return state.value;
2431
+ recordDependency2(state2);
2432
+ return state2.value;
1935
2433
  }
1936
- get.__signal = state;
2434
+ get.__signal = state2;
1937
2435
  if (debugName) get.__name = debugName;
1938
2436
  let set;
1939
2437
  if (equalsFn) {
1940
2438
  set = (next) => {
1941
- const prev = state.value;
2439
+ const prev = state2.value;
1942
2440
  const newValue = typeof next === "function" ? next(prev) : next;
1943
2441
  if (equalsFn(prev, newValue)) return;
1944
- state.value = newValue;
1945
- state.__v++;
1946
- if (_isDev8) {
2442
+ state2.value = newValue;
2443
+ state2.__v++;
2444
+ if (_isDev7) {
1947
2445
  const hook = _g.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1948
- if (hook) hook.emit("signal:update", { signal: state, name: debugName, oldValue: prev, newValue });
2446
+ if (hook) hook.emit("signal:update", { signal: state2, name: debugName, oldValue: prev, newValue });
1949
2447
  }
1950
- if (!enqueueBatchedSignal(state)) {
1951
- notifySubscribers2(state);
2448
+ if (!enqueueBatchedSignal(state2)) {
2449
+ notifySubscribers2(state2);
1952
2450
  }
1953
2451
  };
1954
- } else if (_isDev8) {
2452
+ } else if (_isDev7) {
1955
2453
  set = (next) => {
1956
- const prev = state.value;
2454
+ const prev = state2.value;
1957
2455
  const newValue = typeof next === "function" ? next(prev) : next;
1958
2456
  if (Object.is(newValue, prev)) return;
1959
- state.value = newValue;
1960
- state.__v++;
2457
+ state2.value = newValue;
2458
+ state2.__v++;
1961
2459
  const hook = _g.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1962
- if (hook) hook.emit("signal:update", { signal: state, name: debugName, oldValue: prev, newValue });
1963
- if (!enqueueBatchedSignal(state)) {
1964
- notifySubscribers2(state);
2460
+ if (hook) hook.emit("signal:update", { signal: state2, name: debugName, oldValue: prev, newValue });
2461
+ if (!enqueueBatchedSignal(state2)) {
2462
+ notifySubscribers2(state2);
1965
2463
  }
1966
2464
  };
1967
2465
  } else {
1968
2466
  set = (next) => {
1969
- const prev = state.value;
2467
+ const prev = state2.value;
1970
2468
  const newValue = typeof next === "function" ? next(prev) : next;
1971
2469
  if (Object.is(newValue, prev)) return;
1972
- state.value = newValue;
1973
- state.__v++;
1974
- if (!enqueueBatchedSignal(state)) {
1975
- notifySubscribers2(state);
2470
+ state2.value = newValue;
2471
+ state2.__v++;
2472
+ if (!enqueueBatchedSignal(state2)) {
2473
+ notifySubscribers2(state2);
1976
2474
  }
1977
2475
  };
1978
2476
  }
1979
- if (_isDev8) {
2477
+ if (_isDev7) {
1980
2478
  const hook = _g.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1981
- if (hook) hook.emit("signal:create", { signal: state, name: debugName, getter: get, initial });
2479
+ if (hook) hook.emit("signal:create", { signal: state2, name: debugName, getter: get, initial });
1982
2480
  }
1983
2481
  return [get, set];
1984
2482
  }
1985
2483
 
2484
+ // src/plugins/i18n.ts
2485
+ init_ssr_context();
2486
+
1986
2487
  // src/utils/globalSingleton.ts
1987
2488
  function globalSingleton(key, create) {
1988
2489
  const g2 = globalThis;
@@ -1992,37 +2493,65 @@ function globalSingleton(key, create) {
1992
2493
  // src/plugins/i18n.ts
1993
2494
  var _i18n = globalSingleton(/* @__PURE__ */ Symbol.for("sibujs.i18n.v1"), () => ({
1994
2495
  locale: signal("en"),
1995
- locales: {}
2496
+ locales: /* @__PURE__ */ Object.create(null)
1996
2497
  }));
1997
- var [currentLocale, setLocaleInternal] = _i18n.locale;
2498
+ var [clientLocale, setClientLocale] = _i18n.locale;
1998
2499
  var locales = _i18n.locales;
2500
+ function dictionaryFor(locale) {
2501
+ return Object.hasOwn(locales, locale) ? locales[locale] : void 0;
2502
+ }
2503
+ function lookup(key) {
2504
+ const dictionary = dictionaryFor(getLocale());
2505
+ if (dictionary === void 0 || !Object.hasOwn(dictionary, key)) return void 0;
2506
+ const message = dictionary[key];
2507
+ return typeof message === "string" ? message : void 0;
2508
+ }
1999
2509
  function setLocale(locale) {
2000
- setLocaleInternal(locale);
2510
+ const request = getRequestStore();
2511
+ if (request) {
2512
+ request.locale = locale;
2513
+ return;
2514
+ }
2515
+ setClientLocale(locale);
2001
2516
  }
2002
2517
  function getLocale() {
2003
- return currentLocale();
2518
+ const request = getRequestStore();
2519
+ if (request) {
2520
+ return request.locale ?? clientLocale();
2521
+ }
2522
+ return clientLocale();
2004
2523
  }
2005
2524
  function registerTranslations(locale, messages) {
2006
- locales[locale] = { ...locales[locale], ...messages };
2525
+ const prepared = { ...messages };
2526
+ const merged = Object.assign(/* @__PURE__ */ Object.create(null), dictionaryFor(locale), prepared);
2527
+ Object.defineProperty(locales, locale, {
2528
+ value: merged,
2529
+ writable: true,
2530
+ enumerable: true,
2531
+ configurable: true
2532
+ });
2007
2533
  }
2008
2534
  function t(key, params) {
2009
- const locale = currentLocale();
2010
- const message = locales[locale]?.[key] || key;
2535
+ const message = lookup(key) ?? key;
2011
2536
  return params ? message.replace(/\{(\w+)\}/g, (_, p2) => String(params[p2] ?? "")) : message;
2012
2537
  }
2013
2538
  function Trans(key, params) {
2014
2539
  return span(() => t(key, params));
2015
2540
  }
2016
2541
  function hasTranslation(key) {
2017
- const locale = currentLocale();
2018
- return key in (locales[locale] || {});
2542
+ return lookup(key) !== void 0;
2019
2543
  }
2020
2544
  function getAvailableLocales() {
2021
2545
  return Object.keys(locales);
2022
2546
  }
2023
2547
 
2548
+ // src/plugins/router.ts
2549
+ init_dev();
2550
+ init_dispose();
2551
+
2024
2552
  // src/core/signals/effect.ts
2025
2553
  init_dev();
2554
+ init_errors();
2026
2555
  init_ssr_context();
2027
2556
  var _g2 = globalThis;
2028
2557
  var MAX_RERUNS = 100;
@@ -2034,7 +2563,7 @@ function flushUserCleanups(ctx) {
2034
2563
  try {
2035
2564
  list[i2]();
2036
2565
  } catch (err) {
2037
- if (typeof console !== "undefined") console.warn("[SibuJS effect] onCleanup threw:", err);
2566
+ reportError(err, { phase: "cleanup" });
2038
2567
  }
2039
2568
  }
2040
2569
  }
@@ -2047,11 +2576,12 @@ function drainReruns(ctx) {
2047
2576
  } while (ctx.rerunPending && ++reruns <= MAX_RERUNS);
2048
2577
  if (ctx.rerunPending) {
2049
2578
  ctx.rerunPending = false;
2050
- if (_g2.__SIBU_DEV_WARN__ !== false && typeof console !== "undefined") {
2051
- console.error(
2052
- `[SibuJS] effect re-requested itself ${MAX_RERUNS}+ times \u2014 likely a write-reads-self cycle. Breaking to prevent infinite loop.`
2053
- );
2054
- }
2579
+ reportError(
2580
+ new Error(
2581
+ `effect re-requested itself ${MAX_RERUNS}+ times \u2014 likely a write-reads-self cycle. Breaking to prevent infinite loop.`
2582
+ ),
2583
+ { phase: "effect" }
2584
+ );
2055
2585
  }
2056
2586
  }
2057
2587
  function disposeEffect(ctx) {
@@ -2128,6 +2658,8 @@ function effect(effectFn, options) {
2128
2658
  sub2.depsTail = null;
2129
2659
  sub2._epoch = 0;
2130
2660
  sub2._structDirty = false;
2661
+ sub2._hasComputedDep = false;
2662
+ sub2._errorPhase = "effect";
2131
2663
  sub2._runEpoch = 0;
2132
2664
  sub2._runs = 0;
2133
2665
  ctx.subscriber = sub2;
@@ -2146,12 +2678,32 @@ function effect(effectFn, options) {
2146
2678
 
2147
2679
  // src/plugins/router.ts
2148
2680
  init_sanitize();
2149
- function isSafeNavigationTarget(path2) {
2150
- if (path2 === "") return true;
2681
+ function classifyNavigationTarget(path2) {
2682
+ if (path2 === "") return "internal";
2151
2683
  const normalized = stripControlChars(path2).replace(/\\/g, "/");
2152
- if (normalized.startsWith("//")) return false;
2153
- return sanitizeUrl(normalized) !== "";
2684
+ if (normalized === "") return "internal";
2685
+ if (normalized.startsWith("//")) return "unsafe";
2686
+ if (!/^[a-z][a-z0-9+.-]*:/i.test(normalized)) return "internal";
2687
+ return sanitizeUrl(normalized) === "" ? "unsafe" : "external";
2688
+ }
2689
+ function isRouterNavigable(path2) {
2690
+ return classifyNavigationTarget(path2) === "internal";
2154
2691
  }
2692
+ function normalizePathname(path2) {
2693
+ if (!path2) return "/";
2694
+ const trimmed = path2.length > 1 ? path2.replace(/\/+$/, "") : path2;
2695
+ return trimmed || "/";
2696
+ }
2697
+ function normalizeQuery(query) {
2698
+ const params = new URLSearchParams(query);
2699
+ return [...params.entries()].sort(([a2], [b2]) => a2 < b2 ? -1 : a2 > b2 ? 1 : 0).map(([k, v]) => `${k}=${v}`).join("&");
2700
+ }
2701
+ function isPathnameAncestor(target, current) {
2702
+ if (target === current) return true;
2703
+ if (target === "/") return false;
2704
+ return current.startsWith(`${target}/`);
2705
+ }
2706
+ var ABORT_ROUTER_DESTROYED = "sibu:router-destroyed";
2155
2707
  var LRUCache = class {
2156
2708
  constructor(maxSize = 100) {
2157
2709
  this.cache = /* @__PURE__ */ new Map();
@@ -2212,9 +2764,9 @@ var NavigationController = class {
2212
2764
  }
2213
2765
  }
2214
2766
  }
2215
- abort() {
2767
+ abort(reason) {
2216
2768
  if (this.currentController) {
2217
- this.currentController.abort();
2769
+ this.currentController.abort(reason);
2218
2770
  this.currentController = null;
2219
2771
  }
2220
2772
  }
@@ -2400,6 +2952,7 @@ var GuardManager = class {
2400
2952
  this.timeout = timeout;
2401
2953
  }
2402
2954
  async runBeforeEach(to, from, signal2) {
2955
+ if (signal2.aborted) throw new Error("Navigation aborted");
2403
2956
  for (const guard of this.beforeEachGuards) {
2404
2957
  if (signal2.aborted) throw new Error("Navigation aborted");
2405
2958
  const result = await this.runGuard(guard, to, from, signal2);
@@ -2408,6 +2961,7 @@ var GuardManager = class {
2408
2961
  return true;
2409
2962
  }
2410
2963
  async runBeforeResolve(to, from, signal2) {
2964
+ if (signal2.aborted) throw new Error("Navigation aborted");
2411
2965
  for (const guard of this.beforeResolveGuards) {
2412
2966
  if (signal2.aborted) throw new Error("Navigation aborted");
2413
2967
  const result = await this.runGuard(guard, to, from, signal2);
@@ -2504,13 +3058,16 @@ var _ComponentLoader = class _ComponentLoader {
2504
3058
  constructor(cacheSize = 50, retryDelay = 1e3) {
2505
3059
  this.errorCache = /* @__PURE__ */ new Map();
2506
3060
  this.loadingPromises = /* @__PURE__ */ new Map();
3061
+ // In-flight *module* resolutions for deferred plans, so concurrent first
3062
+ // mounts of a lazy route do not each run the loader arrow.
3063
+ this.moduleLoads = /* @__PURE__ */ new Map();
2507
3064
  // Stable per-route-definition id. Caching by the RESOLVED path (e.g.
2508
3065
  // /users/123) gave the cache one entry per visited URL, so parameterized
2509
3066
  // routes thrashed/evicted and the component was reloaded every navigation.
2510
3067
  // Keying by route-definition identity makes the cache effective again.
2511
3068
  this.routeKeys = /* @__PURE__ */ new WeakMap();
2512
3069
  this.keyCounter = 0;
2513
- this.componentCache = new LRUCache(cacheSize);
3070
+ this.planCache = new LRUCache(cacheSize);
2514
3071
  this.retryDelay = retryDelay;
2515
3072
  }
2516
3073
  keyFor(route2) {
@@ -2521,91 +3078,233 @@ var _ComponentLoader = class _ComponentLoader {
2521
3078
  }
2522
3079
  return key;
2523
3080
  }
2524
- async loadComponent(route2, routePath) {
3081
+ /**
3082
+ * Resolve the route's **plan** — how to produce an instance — without ever
3083
+ * instantiating a component to inspect it.
3084
+ *
3085
+ * Loading a route component and instantiating one are separate operations.
3086
+ * The loader never invokes a user component factory merely to validate its
3087
+ * return type; validation happens on the instance that will actually be
3088
+ * mounted. (LOAD-001)
3089
+ */
3090
+ async loadPlan(route2, routePath) {
2525
3091
  if (!("component" in route2)) {
2526
3092
  throw new Error(`Route ${routePath} does not have a component`);
2527
3093
  }
2528
3094
  const comp = route2.component;
2529
3095
  const cacheKey = this.keyFor(route2);
2530
- const cached = this.componentCache.get(cacheKey);
3096
+ const cached = this.planCache.get(cacheKey);
2531
3097
  if (cached) return cached;
2532
3098
  const existingPromise = this.loadingPromises.get(cacheKey);
2533
3099
  if (existingPromise) return existingPromise;
2534
- const errorInfo = this.errorCache.get(cacheKey);
2535
- if (errorInfo && Date.now() - errorInfo.timestamp < this.retryDelay) {
2536
- throw new Error(`Component loading failed recently, retry in ${this.retryDelay}ms`);
2537
- }
2538
- const loadingPromise = this.doLoadComponent(comp, routePath);
3100
+ this.assertNotHot(cacheKey);
3101
+ const loadingPromise = this.doLoadPlan(comp, routePath);
2539
3102
  this.loadingPromises.set(cacheKey, loadingPromise);
2540
3103
  try {
2541
- const component = await loadingPromise;
2542
- this.componentCache.set(cacheKey, component);
3104
+ const plan = await loadingPromise;
3105
+ this.planCache.set(cacheKey, plan);
2543
3106
  this.errorCache.delete(cacheKey);
2544
- return component;
3107
+ return plan;
2545
3108
  } catch (error) {
2546
- const currentError = this.errorCache.get(cacheKey) || { timestamp: 0, count: 0 };
2547
- if (!this.errorCache.has(cacheKey) && this.errorCache.size >= _ComponentLoader.MAX_ERROR_ENTRIES) {
2548
- const oldest = this.errorCache.keys().next().value;
2549
- if (oldest !== void 0) this.errorCache.delete(oldest);
2550
- }
2551
- this.errorCache.set(cacheKey, {
2552
- timestamp: Date.now(),
2553
- count: currentError.count + 1
2554
- });
3109
+ this.noteError(cacheKey);
2555
3110
  throw error;
2556
3111
  } finally {
2557
3112
  this.loadingPromises.delete(cacheKey);
2558
3113
  }
2559
3114
  }
2560
- async doLoadComponent(comp, routePath) {
2561
- if (!this.isAsyncComponent(comp)) {
2562
- const result = comp();
2563
- if (!this.isElement(result)) {
2564
- throw new Error(`Component for route "${routePath}" must return Element, got ${typeof result}`);
2565
- }
2566
- return comp;
3115
+ /** Reject early while a recent load failure for this route is still hot. */
3116
+ assertNotHot(cacheKey) {
3117
+ const errorInfo = this.errorCache.get(cacheKey);
3118
+ if (errorInfo && Date.now() - errorInfo.timestamp < this.retryDelay) {
3119
+ throw new Error(`Component loading failed recently, retry in ${this.retryDelay}ms`);
3120
+ }
3121
+ }
3122
+ /**
3123
+ * Record a load failure. Keyed by route-definition id, so the map is bounded
3124
+ * by the number of route definitions; still capped defensively, evicting the
3125
+ * oldest.
3126
+ */
3127
+ noteError(cacheKey) {
3128
+ const currentError = this.errorCache.get(cacheKey) || { timestamp: 0, count: 0 };
3129
+ if (!this.errorCache.has(cacheKey) && this.errorCache.size >= _ComponentLoader.MAX_ERROR_ENTRIES) {
3130
+ const oldest = this.errorCache.keys().next().value;
3131
+ if (oldest !== void 0) this.errorCache.delete(oldest);
3132
+ }
3133
+ this.errorCache.set(cacheKey, { timestamp: Date.now(), count: currentError.count + 1 });
3134
+ }
3135
+ /**
3136
+ * Classify without instantiating.
3137
+ *
3138
+ * The `lazy()` brand decides, and nothing else: a branded loader is
3139
+ * `deferred`, everything else is a `factory`. No inspection of the function's
3140
+ * type or source is involved, so an unbranded factory can never become
3141
+ * preload-executable. (LOAD-004)
3142
+ *
3143
+ * An unbranded factory that happens to resolve to a module — an un-marked
3144
+ * `async () => ({ default: Page })` — still works: that is unwrapped during
3145
+ * real instantiation, where the produced value is used rather than discarded.
3146
+ */
3147
+ async doLoadPlan(comp, routePath) {
3148
+ if (typeof comp !== "function") {
3149
+ throw new Error(`Component for route "${routePath}" must be a function, got ${typeof comp}`);
3150
+ }
3151
+ if (!this.isDeferredModule(comp)) {
3152
+ return { kind: "factory", component: comp };
3153
+ }
3154
+ return { kind: "deferred", load: comp };
3155
+ }
3156
+ /**
3157
+ * Create **one** real route component instance, validating what will actually
3158
+ * be mounted.
3159
+ *
3160
+ * `route` is used only to upgrade a `deferred` plan once a lazy module has
3161
+ * resolved, so the import happens once and later mounts call the module's
3162
+ * factory directly.
3163
+ */
3164
+ async instantiate(plan, route2, routePath) {
3165
+ if (plan.kind === "factory") {
3166
+ return this.instantiateFactory(plan.component, route2, routePath);
2567
3167
  }
3168
+ const factory = await this.resolveModuleFactory(plan, route2, routePath);
3169
+ return this.instantiateFactory(factory, route2, routePath);
3170
+ }
3171
+ /**
3172
+ * Resolve a deferred plan's module and cache the factory it exports —
3173
+ * **without invoking that factory**.
3174
+ *
3175
+ * Shared by instantiation and preloading, so a preload racing a navigation to
3176
+ * the same route imports the module once. A deferred plan is only ever a
3177
+ * module loader (see `isDeferredModule`), so a resolution that is not
3178
+ * module-shaped is a configuration error and is reported as one.
3179
+ */
3180
+ async resolveModuleFactory(plan, route2, routePath) {
3181
+ const cacheKey = this.keyFor(route2);
3182
+ const cached = this.planCache.get(cacheKey);
3183
+ if (cached && cached.kind === "factory") return cached.component;
3184
+ this.assertNotHot(cacheKey);
3185
+ let pending = this.moduleLoads.get(cacheKey);
3186
+ const owner = pending === void 0;
3187
+ if (pending === void 0) {
3188
+ pending = plan.load();
3189
+ this.moduleLoads.set(cacheKey, pending);
3190
+ }
3191
+ let resolved;
2568
3192
  try {
2569
- const result = await comp();
2570
- const component = this.extractComponent(result, routePath);
2571
- const testElement = component();
2572
- if (!this.isElement(testElement)) {
2573
- throw new Error(`Component for route "${routePath}" must return Element, got ${typeof testElement}`);
2574
- }
2575
- return component;
3193
+ resolved = await pending;
2576
3194
  } catch (error) {
2577
- const wrapped = new Error(
2578
- `Failed to load component for route "${routePath}": ${error instanceof Error ? error.message : String(error)}`
2579
- );
2580
- wrapped.cause = error;
2581
- throw wrapped;
3195
+ this.noteError(cacheKey);
3196
+ throw error;
3197
+ } finally {
3198
+ if (owner) this.moduleLoads.delete(cacheKey);
2582
3199
  }
3200
+ const factory = this.extractFactory(resolved, routePath);
3201
+ this.planCache.set(cacheKey, { kind: "factory", component: factory });
3202
+ return factory;
2583
3203
  }
2584
- isAsyncComponent(comp) {
2585
- return comp[LAZY_MARKER] === true || comp.constructor.name === "AsyncFunction" || typeof comp === "function" && comp.toString().includes("import(");
3204
+ /**
3205
+ * Invoke a factory exactly once and validate the instance it produced.
3206
+ *
3207
+ * A factory may turn out to resolve to a *module* rather than an Element —
3208
+ * an un-marked `async () => ({ default: Page })` is indistinguishable from a
3209
+ * component factory until it runs. That is settled here, on the real
3210
+ * invocation, and the plan is upgraded so later mounts skip the extra hop.
3211
+ * `depth` bounds that unwrapping; a module whose default is itself a module
3212
+ * is a configuration error, not something to chase.
3213
+ */
3214
+ async instantiateFactory(component, route2, routePath, depth = 0) {
3215
+ const result = component();
3216
+ const value = this.isThenable(result) ? await result : result;
3217
+ if (this.isElement(value)) return value;
3218
+ if (depth === 0 && this.isFactoryLike(value)) {
3219
+ const factory = this.extractFactory(value, routePath);
3220
+ this.planCache.set(this.keyFor(route2), { kind: "factory", component: factory });
3221
+ return this.instantiateFactory(factory, route2, routePath, depth + 1);
3222
+ }
3223
+ throw new Error(`Component for route "${routePath}" must return Element, got ${typeof value}`);
2586
3224
  }
2587
- isElement(value) {
2588
- return value instanceof Element;
3225
+ /**
3226
+ * Something `extractFactory` can turn into a component factory: a bare
3227
+ * callable, or a module namespace carrying a callable `default`. Both are
3228
+ * long-supported shapes for an async loader's resolution.
3229
+ */
3230
+ isFactoryLike(value) {
3231
+ if (typeof value === "function") return true;
3232
+ return typeof value === "object" && value !== null && typeof value.default === "function";
2589
3233
  }
2590
- extractComponent(result, routePath) {
2591
- if ("default" in result && typeof result.default === "function") {
3234
+ /**
3235
+ * Pull a factory out of a resolved lazy module **without calling it**. Only
3236
+ * what can be checked without running user code is checked here: that a
3237
+ * default export exists and is callable.
3238
+ */
3239
+ extractFactory(result, routePath) {
3240
+ if (typeof result === "function") return result;
3241
+ if (typeof result === "object" && result !== null && typeof result.default === "function") {
2592
3242
  return result.default;
2593
3243
  }
2594
- if (typeof result === "function") {
2595
- return result;
2596
- }
2597
- if (this.isElement(result)) {
2598
- return () => result;
2599
- }
2600
3244
  throw new Error(`Invalid component module for route "${routePath}"`);
2601
3245
  }
3246
+ /**
3247
+ * Prepare explicitly deferred route code without creating a component
3248
+ * instance or any DOM.
3249
+ *
3250
+ * Only routes explicitly branded as deferred — a loader wrapped in `lazy()` —
3251
+ * are resolved during preload. Direct component factories, whether
3252
+ * synchronous, `async`, or Promise-returning, are `factory` plans and
3253
+ * therefore require no preload work at all.
3254
+ *
3255
+ * ```text
3256
+ * factory plan → preload no-op
3257
+ * deferred plan → resolve module → cache factory → no component invocation
3258
+ * ```
3259
+ *
3260
+ * Deferred module resolution caches the component factory the module exports
3261
+ * **without invoking it**. (LOAD-004)
3262
+ */
3263
+ async preloadPlan(route2, routePath) {
3264
+ const plan = await this.loadPlan(route2, routePath);
3265
+ if (plan.kind === "factory") return;
3266
+ await this.resolveModuleFactory(plan, route2, routePath);
3267
+ }
3268
+ isThenable(value) {
3269
+ return (typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function";
3270
+ }
3271
+ /**
3272
+ * Is this route component an **explicitly declared module loader**?
3273
+ *
3274
+ * Preload is the only place SibuJS executes a route function before the
3275
+ * router needs an instance, so permission to do that must be explicit. The
3276
+ * brand stamped by `lazy()` is the sole authority.
3277
+ *
3278
+ * Everything the loader once inferred is deliberately *not* a signal:
3279
+ *
3280
+ * - `constructor.name === "AsyncFunction"` — cannot distinguish
3281
+ * `async () => import("./Page")` from `async () => div()`. Treating the
3282
+ * latter as a module loader is what made preload invoke user
3283
+ * components. (LOAD-003)
3284
+ * - `toString().includes("import(")` — source text is a representation, not
3285
+ * semantics. An ordinary component with `"import('./x')"` in a string, a
3286
+ * template literal, or a comment was executed during preload; and the same
3287
+ * text can vanish under a bundler that rewrites dynamic imports into its
3288
+ * own chunk-loader calls. Both directions are wrong, and no better regex
3289
+ * fixes that — the defect is source inspection itself. (LOAD-004)
3290
+ *
3291
+ * A brand is metadata: it survives TypeScript, every bundler, and
3292
+ * minification, because it is part of runtime behaviour rather than syntax.
3293
+ */
3294
+ isDeferredModule(comp) {
3295
+ return comp[LAZY_MARKER] === true;
3296
+ }
3297
+ isElement(value) {
3298
+ return value instanceof Element;
3299
+ }
2602
3300
  clearErrors() {
2603
3301
  this.errorCache.clear();
2604
3302
  }
2605
3303
  clearCache() {
2606
- this.componentCache.clear();
3304
+ this.planCache.clear();
2607
3305
  this.errorCache.clear();
2608
3306
  this.loadingPromises.clear();
3307
+ this.moduleLoads.clear();
2609
3308
  }
2610
3309
  };
2611
3310
  _ComponentLoader.MAX_ERROR_ENTRIES = 256;
@@ -2614,6 +3313,17 @@ var _SibuRouter = class _SibuRouter {
2614
3313
  constructor(routes, options = {}) {
2615
3314
  // Event listeners cleanup
2616
3315
  this.cleanup = [];
3316
+ // Set by destroy(). Lets a navigation still in flight report
3317
+ // "router-destroyed" rather than the generic "superseded".
3318
+ this.destroyed = false;
3319
+ // Monotonic navigation generation. Bumped by every committed navigation that
3320
+ // can acquire DOM ownership, and by destroy(). Async work that wants to
3321
+ // commit into the DOM captures this first and re-checks before committing:
3322
+ // unlike a URL comparison it distinguishes "/b" generation 10 from "/b"
3323
+ // generation 12, so an A→B→A round trip can never restore commit permission
3324
+ // to superseded work. The router's own initial location resolution is
3325
+ // excluded — it establishes the bootstrap route rather than superseding it.
3326
+ this.navEpoch = 0;
2617
3327
  this.options = {
2618
3328
  mode: "history",
2619
3329
  base: "",
@@ -2652,7 +3362,7 @@ var _SibuRouter = class _SibuRouter {
2652
3362
  }
2653
3363
  queueMicrotask(() => {
2654
3364
  if (!this.navigator.isNavigating) {
2655
- this.handleLocationChange(true);
3365
+ this.handleLocationChange(true, true);
2656
3366
  }
2657
3367
  this.isReadySetter(true);
2658
3368
  });
@@ -2667,14 +3377,15 @@ var _SibuRouter = class _SibuRouter {
2667
3377
  matched: []
2668
3378
  };
2669
3379
  }
2670
- handleLocationChange(skipHistory = true) {
3380
+ handleLocationChange(skipHistory = true, initialResolution = false) {
2671
3381
  const path2 = this.getCurrentPath();
2672
- this.navigate(path2, { replace: true, skipHistory }).catch((err) => {
3382
+ this.navigate(path2, { replace: true, skipHistory, initialResolution }).catch((err) => {
2673
3383
  console.error("[Router] Error during location change navigation:", err);
2674
3384
  });
2675
3385
  }
2676
3386
  getCurrentPath() {
2677
3387
  const { mode, base: base2 } = this.options;
3388
+ if (typeof window === "undefined") return "/";
2678
3389
  if (mode === "hash") {
2679
3390
  return window.location.hash.slice(1) || "/";
2680
3391
  }
@@ -2706,15 +3417,15 @@ var _SibuRouter = class _SibuRouter {
2706
3417
  try {
2707
3418
  await this.navigator.navigate(async (signal2) => {
2708
3419
  const targetPath = this.resolvePath(to);
2709
- if (!isSafeNavigationTarget(targetPath)) {
3420
+ if (!isRouterNavigable(targetPath)) {
2710
3421
  const from2 = this.currentRouteGetter();
2711
3422
  const toContext2 = this.createRouteContext(targetPath);
2712
- throw new NavigationFailureError("aborted", from2, toContext2);
3423
+ throw new NavigationFailureError("aborted", from2, toContext2, void 0, "unsafe-target");
2713
3424
  }
2714
3425
  const from = this.currentRouteGetter();
2715
3426
  const toContext = this.createRouteContext(targetPath);
2716
3427
  if (this.isSameRoute(from, toContext)) {
2717
- throw new NavigationFailureError("duplicated", from, toContext);
3428
+ throw new NavigationFailureError("duplicated", from, toContext, void 0, "duplicate");
2718
3429
  }
2719
3430
  await this.performNavigation(toContext, from, options, signal2);
2720
3431
  });
@@ -2722,30 +3433,43 @@ var _SibuRouter = class _SibuRouter {
2722
3433
  } catch (error) {
2723
3434
  if (error instanceof NavigationFailureError) {
2724
3435
  const failure2 = error.toFailure();
2725
- return { success: false, type: failure2.type, failure: failure2 };
3436
+ return { success: false, type: failure2.type, reason: failure2.reason, failure: failure2 };
2726
3437
  }
3438
+ const isAbort = error instanceof Error && error.message === "Navigation aborted";
3439
+ const reason = isAbort ? this.destroyed ? "router-destroyed" : "superseded" : "error";
2727
3440
  const failure = {
2728
3441
  type: "aborted",
3442
+ reason,
2729
3443
  from: this.currentRouteGetter(),
2730
3444
  to: this.createRouteContext(this.resolvePath(to)),
2731
3445
  error: error instanceof Error ? error : new Error(String(error))
2732
3446
  };
2733
- return { success: false, type: failure.type, failure };
3447
+ return { success: false, type: failure.type, reason, failure };
2734
3448
  }
2735
3449
  }
2736
- async performNavigation(to, from, options, signal2, depth = 0) {
3450
+ async performNavigation(to, from, options, signal2, depth = 0, redirectChain = []) {
3451
+ const chain = depth === 0 ? [to.path] : redirectChain;
2737
3452
  if (depth >= _SibuRouter.MAX_REDIRECT_DEPTH) {
2738
- throw new NavigationFailureError("aborted", from, to);
3453
+ if (isDev()) {
3454
+ devWarn(
3455
+ `Router: redirect loop detected \u2014 navigation aborted after ${_SibuRouter.MAX_REDIRECT_DEPTH} hops.
3456
+ ${chain.map((p2) => `"${p2}"`).join(" -> ")}`
3457
+ );
3458
+ }
3459
+ throw new NavigationFailureError("aborted", from, to, void 0, "redirect-loop");
2739
3460
  }
2740
3461
  const beforeEachResult = await this.guards.runBeforeEach(to, from, signal2);
2741
3462
  if (beforeEachResult !== true) {
2742
3463
  if (typeof beforeEachResult === "string") {
2743
- if (!isSafeNavigationTarget(beforeEachResult)) {
2744
- throw new NavigationFailureError("aborted", from, to);
3464
+ if (!isRouterNavigable(beforeEachResult)) {
3465
+ throw new NavigationFailureError("aborted", from, to, void 0, "unsafe-target");
2745
3466
  }
2746
- return this.performNavigation(this.createRouteContext(beforeEachResult), from, options, signal2, depth + 1);
3467
+ return this.performNavigation(this.createRouteContext(beforeEachResult), from, options, signal2, depth + 1, [
3468
+ ...chain,
3469
+ beforeEachResult
3470
+ ]);
2747
3471
  }
2748
- throw new NavigationFailureError("aborted", from, to);
3472
+ throw new NavigationFailureError("aborted", from, to, void 0, "guard");
2749
3473
  }
2750
3474
  const match = this.matcher.match(to.path);
2751
3475
  if (match) {
@@ -2756,47 +3480,60 @@ var _SibuRouter = class _SibuRouter {
2756
3480
  for (const guard of guards) {
2757
3481
  if (signal2.aborted) throw new Error("Navigation aborted");
2758
3482
  const result = await guard(to, from);
3483
+ if (signal2.aborted) {
3484
+ throw new NavigationFailureError("aborted", from, to, void 0, abortReasonOf(signal2));
3485
+ }
2759
3486
  if (result !== true) {
2760
3487
  if (typeof result === "string") {
2761
- if (!isSafeNavigationTarget(result)) {
2762
- throw new NavigationFailureError("aborted", from, to);
3488
+ if (!isRouterNavigable(result)) {
3489
+ throw new NavigationFailureError("aborted", from, to, void 0, "unsafe-target");
2763
3490
  }
2764
- return this.performNavigation(this.createRouteContext(result), from, options, signal2, depth + 1);
3491
+ return this.performNavigation(this.createRouteContext(result), from, options, signal2, depth + 1, [
3492
+ ...chain,
3493
+ result
3494
+ ]);
2765
3495
  }
2766
- throw new NavigationFailureError("aborted", from, to);
3496
+ throw new NavigationFailureError("aborted", from, to, void 0, "guard");
2767
3497
  }
2768
3498
  }
2769
3499
  }
2770
3500
  }
2771
3501
  if ("redirect" in route2) {
2772
3502
  const redirectPath = typeof route2.redirect === "function" ? route2.redirect(to) : route2.redirect;
2773
- if (typeof redirectPath === "string" && /^(https?:)?\/\//i.test(redirectPath)) {
2774
- if (typeof console !== "undefined") {
3503
+ if (typeof redirectPath === "string" && !isRouterNavigable(redirectPath)) {
3504
+ if (classifyNavigationTarget(redirectPath) === "external" && typeof console !== "undefined") {
2775
3505
  console.error(
2776
3506
  `[SibuJS Router] Refusing absolute/protocol-relative redirect "${redirectPath}" \u2014 open-redirect risk.`
2777
3507
  );
2778
3508
  }
2779
- throw new NavigationFailureError("aborted", from, to);
2780
- }
2781
- if (typeof redirectPath === "string" && !isSafeNavigationTarget(redirectPath)) {
2782
- throw new NavigationFailureError("aborted", from, to);
3509
+ throw new NavigationFailureError("aborted", from, to, void 0, "unsafe-target");
2783
3510
  }
2784
- return this.performNavigation(this.createRouteContext(redirectPath), from, options, signal2, depth + 1);
3511
+ return this.performNavigation(this.createRouteContext(redirectPath), from, options, signal2, depth + 1, [
3512
+ ...chain,
3513
+ redirectPath
3514
+ ]);
2785
3515
  }
2786
3516
  }
2787
3517
  const beforeResolveResult = await this.guards.runBeforeResolve(to, from, signal2);
2788
3518
  if (beforeResolveResult !== true) {
2789
3519
  if (typeof beforeResolveResult === "string") {
2790
- if (!isSafeNavigationTarget(beforeResolveResult)) {
2791
- throw new NavigationFailureError("aborted", from, to);
3520
+ if (!isRouterNavigable(beforeResolveResult)) {
3521
+ throw new NavigationFailureError("aborted", from, to, void 0, "unsafe-target");
2792
3522
  }
2793
- return this.performNavigation(this.createRouteContext(beforeResolveResult), from, options, signal2, depth + 1);
3523
+ return this.performNavigation(this.createRouteContext(beforeResolveResult), from, options, signal2, depth + 1, [
3524
+ ...chain,
3525
+ beforeResolveResult
3526
+ ]);
2794
3527
  }
2795
- throw new NavigationFailureError("aborted", from, to);
3528
+ throw new NavigationFailureError("aborted", from, to, void 0, "guard");
3529
+ }
3530
+ if (signal2.aborted) {
3531
+ throw new NavigationFailureError("aborted", from, to, void 0, abortReasonOf(signal2));
2796
3532
  }
2797
3533
  if (!options.skipHistory) {
2798
3534
  this.updateHistory(to, options);
2799
3535
  }
3536
+ if (!options.initialResolution) this.navEpoch++;
2800
3537
  this.currentRouteSetter(to);
2801
3538
  this.guards.runAfterEach(to, from);
2802
3539
  this.handleScrollBehavior(to, from);
@@ -2829,25 +3566,71 @@ var _SibuRouter = class _SibuRouter {
2829
3566
  }
2830
3567
  updateHistory(to, options) {
2831
3568
  const fullPath = this.options.base + to.path + (Object.keys(to.query).length ? `?${new URLSearchParams(to.query).toString()}` : "") + (to.hash ? `#${to.hash}` : "");
3569
+ const h = globalThis.history;
3570
+ if (!h) return;
2832
3571
  if (options.replace) {
2833
- history.replaceState(options.state || {}, "", fullPath);
3572
+ h.replaceState(options.state || {}, "", fullPath);
2834
3573
  } else {
2835
- history.pushState(options.state || {}, "", fullPath);
3574
+ h.pushState(options.state || {}, "", fullPath);
2836
3575
  }
2837
3576
  }
3577
+ /**
3578
+ * Apply the configured `scrollBehavior`, if the runtime can actually scroll.
3579
+ *
3580
+ * This runs *after* the route has been committed, so an exception here does
3581
+ * not merely skip scrolling — it propagates out of `navigateInternal` and the
3582
+ * navigation is reported as `success: false` while the route state says it
3583
+ * succeeded. In a DOM-less runtime the bare `requestAnimationFrame` did
3584
+ * exactly that: a legal `scrollBehavior` option made every navigation report
3585
+ * failure under SSR and in memory-router-style tests. (MEM-001)
3586
+ *
3587
+ * Policy A, consistent with the history write in `updateHistory()` (NODE-001):
3588
+ * the route still commits, and only the browser-only side effect is skipped.
3589
+ *
3590
+ * Both primitives are probed, not just `window` — `requestAnimationFrame` and
3591
+ * `scrollTo` can be missing independently (jsdom without a rAF polyfill, a
3592
+ * partial DOM shim). `requestAnimationFrame` is invoked as a method on the
3593
+ * global object rather than through a detached reference, since browsers
3594
+ * reject a bare call with "Illegal invocation".
3595
+ */
2838
3596
  handleScrollBehavior(to, from) {
2839
- if (this.options.scrollBehavior) {
2840
- const scrollTo = this.options.scrollBehavior(to, from, null);
2841
- if (scrollTo) {
2842
- requestAnimationFrame(() => {
2843
- window.scrollTo(scrollTo.x, scrollTo.y);
2844
- });
2845
- }
3597
+ const scrollBehavior = this.options.scrollBehavior;
3598
+ if (!scrollBehavior) return;
3599
+ const g2 = globalThis;
3600
+ if (typeof g2.requestAnimationFrame !== "function") return;
3601
+ if (typeof window === "undefined" || typeof window.scrollTo !== "function") return;
3602
+ let position;
3603
+ try {
3604
+ position = scrollBehavior(to, from, null);
3605
+ } catch (error) {
3606
+ if (typeof console !== "undefined") console.error("[router] scrollBehavior failed:", error);
3607
+ return;
2846
3608
  }
3609
+ if (!position) return;
3610
+ const { x, y } = position;
3611
+ g2.requestAnimationFrame(() => {
3612
+ try {
3613
+ window.scrollTo(x, y);
3614
+ } catch (error) {
3615
+ if (typeof console !== "undefined") console.error("[router] scroll failed:", error);
3616
+ }
3617
+ });
2847
3618
  }
2848
3619
  // Component loading
2849
- async loadComponent(route2, routePath) {
2850
- return this.loader.loadComponent(route2, routePath);
3620
+ /**
3621
+ * Resolve how this route's component will be produced, without instantiating
3622
+ * anything. Pair with {@link instantiateComponent} at the commit boundary.
3623
+ */
3624
+ async loadPlan(route2, routePath) {
3625
+ return this.loader.loadPlan(route2, routePath);
3626
+ }
3627
+ /** Create exactly one real instance from an already-resolved plan. */
3628
+ async instantiateComponent(plan, route2, routePath) {
3629
+ return this.loader.instantiate(plan, route2, routePath);
3630
+ }
3631
+ /** Resolve a route's module/factory without creating any component instance. */
3632
+ async preloadPlan(route2, routePath) {
3633
+ return this.loader.preloadPlan(route2, routePath);
2851
3634
  }
2852
3635
  // Guards API
2853
3636
  beforeEach(guard) {
@@ -2882,12 +3665,18 @@ var _SibuRouter = class _SibuRouter {
2882
3665
  get isReady() {
2883
3666
  return this.isReadyGetter();
2884
3667
  }
3668
+ /** @internal Navigation generation — see `navEpoch`. Not a public API. */
3669
+ get navigationEpoch() {
3670
+ return this.navEpoch;
3671
+ }
2885
3672
  get isNavigating() {
2886
3673
  return this.navigator.isNavigating;
2887
3674
  }
2888
3675
  // Cleanup
2889
3676
  destroy() {
2890
- this.navigator.abort();
3677
+ this.destroyed = true;
3678
+ this.navEpoch++;
3679
+ this.navigator.abort(ABORT_ROUTER_DESTROYED);
2891
3680
  for (const fn of this.cleanup) fn();
2892
3681
  this.cleanup = [];
2893
3682
  this.guards.clear();
@@ -2930,10 +3719,11 @@ var _SibuRouter = class _SibuRouter {
2930
3719
  _SibuRouter.MAX_REDIRECT_DEPTH = 10;
2931
3720
  var SibuRouter = _SibuRouter;
2932
3721
  var NavigationFailureError = class extends Error {
2933
- constructor(type, from, to, error) {
2934
- super(`Navigation ${type}: from ${from.path} to ${to.path}`);
3722
+ constructor(type, from, to, error, reason) {
3723
+ super(`Navigation ${type}${reason ? ` (${reason})` : ""}: from ${from.path} to ${to.path}`);
2935
3724
  this.name = "NavigationFailureError";
2936
3725
  this.type = type;
3726
+ this.reason = reason;
2937
3727
  this.from = from;
2938
3728
  this.to = to;
2939
3729
  if (error) {
@@ -2943,12 +3733,16 @@ var NavigationFailureError = class extends Error {
2943
3733
  toFailure() {
2944
3734
  return {
2945
3735
  type: this.type,
3736
+ reason: this.reason,
2946
3737
  from: this.from,
2947
3738
  to: this.to,
2948
3739
  error: this.cause instanceof Error ? this.cause : void 0
2949
3740
  };
2950
3741
  }
2951
3742
  };
3743
+ function abortReasonOf(signal2) {
3744
+ return signal2.reason === ABORT_ROUTER_DESTROYED ? "router-destroyed" : "superseded";
3745
+ }
2952
3746
  var ROUTER_KEY = /* @__PURE__ */ Symbol.for("sibujs.router.v1");
2953
3747
  var _routerRef = globalThis[ROUTER_KEY] ?? (globalThis[ROUTER_KEY] = { current: null });
2954
3748
  function normalizeRoutes(routes) {
@@ -3052,6 +3846,13 @@ function Route() {
3052
3846
  let errorNode = null;
3053
3847
  let navSeq = 0;
3054
3848
  let currentTopRoute = null;
3849
+ let routeTorn = false;
3850
+ const commitTarget = (seq) => routeTorn || seq !== navSeq ? null : anchor.parentNode;
3851
+ const release = (node) => {
3852
+ if (!node) return;
3853
+ dispose(node);
3854
+ node.parentNode?.removeChild(node);
3855
+ };
3055
3856
  const cleanupNodes = () => {
3056
3857
  [currentNode, loadingNode, errorNode].forEach((node) => {
3057
3858
  if (!node) return;
@@ -3129,7 +3930,7 @@ function Route() {
3129
3930
  anchor.parentNode.insertBefore(errorNode, anchor.nextSibling);
3130
3931
  };
3131
3932
  const update = async () => {
3132
- if (!_routerRef.current) return;
3933
+ if (routeTorn || !_routerRef.current) return;
3133
3934
  const seq = ++navSeq;
3134
3935
  const route2 = _routerRef.current.currentRoute;
3135
3936
  try {
@@ -3158,17 +3959,22 @@ function Route() {
3158
3959
  if (isAsync) {
3159
3960
  showLoading();
3160
3961
  }
3161
- const component = await _routerRef.current.loadComponent(routeDef, route2.path);
3162
- if (seq !== navSeq) return;
3163
- const node = component();
3164
- if (node && anchor.parentNode) {
3962
+ const plan = await _routerRef.current.loadPlan(routeDef, route2.path);
3963
+ if (!commitTarget(seq)) return;
3964
+ const node = await _routerRef.current.instantiateComponent(plan, routeDef, route2.path);
3965
+ const parent = commitTarget(seq);
3966
+ if (!parent) {
3967
+ release(node);
3968
+ return;
3969
+ }
3970
+ if (node) {
3165
3971
  currentTopRoute = routeDef;
3166
3972
  cleanupNodes();
3167
- anchor.parentNode.insertBefore(node, anchor.nextSibling);
3973
+ parent.insertBefore(node, anchor.nextSibling);
3168
3974
  currentNode = node;
3169
3975
  }
3170
3976
  } catch (error) {
3171
- if (seq !== navSeq) return;
3977
+ if (routeTorn || seq !== navSeq) return;
3172
3978
  hideLoading();
3173
3979
  console.error("[Route] Component error:", error);
3174
3980
  showError(error instanceof Error ? error : new Error(String(error)), routeDef);
@@ -3176,6 +3982,7 @@ function Route() {
3176
3982
  }
3177
3983
  } catch (error) {
3178
3984
  if (seq !== navSeq) return;
3985
+ if (routeTorn) return;
3179
3986
  console.error("[Route] Update failed:", error);
3180
3987
  showError(error instanceof Error ? error : new Error(String(error)));
3181
3988
  }
@@ -3192,12 +3999,13 @@ function Route() {
3192
3999
  if (!routeInitialized && anchor.parentNode) wrappedUpdate();
3193
4000
  });
3194
4001
  }
3195
- let routeTorn = false;
3196
4002
  const routeCleanup = () => {
3197
4003
  if (routeTorn) return;
3198
4004
  routeTorn = true;
4005
+ navSeq++;
3199
4006
  routeTeardown();
3200
4007
  cleanupNodes();
4008
+ currentTopRoute = null;
3201
4009
  };
3202
4010
  registerDisposer(anchor, routeCleanup);
3203
4011
  routeCleanups.push(routeCleanup);
@@ -3215,16 +4023,14 @@ function KeepAliveRoute(options) {
3215
4023
  let currentNode = null;
3216
4024
  let currentKey = "";
3217
4025
  let currentCached = false;
3218
- let isUpdating = false;
3219
- let pendingUpdate = false;
4026
+ let updateSeq = 0;
4027
+ let kaTorn = false;
3220
4028
  const update = async () => {
3221
- if (!_routerRef.current) return;
3222
- if (isUpdating) {
3223
- pendingUpdate = true;
3224
- return;
3225
- }
3226
- const route2 = _routerRef.current.currentRoute;
3227
- const match = _routerRef.current["matcher"].match(route2.path);
4029
+ if (kaTorn || !_routerRef.current) return;
4030
+ const seq = ++updateSeq;
4031
+ const router2 = _routerRef.current;
4032
+ const route2 = router2.currentRoute;
4033
+ const match = router2["matcher"].match(route2.path);
3228
4034
  if (!match) return;
3229
4035
  const { route: routeDef } = match;
3230
4036
  if ("redirect" in routeDef) {
@@ -3241,63 +4047,62 @@ function KeepAliveRoute(options) {
3241
4047
  const cacheKey = `${route2.path}${queryStr}${route2.hash ? `#${route2.hash}` : ""}`;
3242
4048
  const shouldCache = !includeNames || routeDef.name != null && includeNames.includes(routeDef.name);
3243
4049
  if (cacheKey === currentKey && currentNode) return;
3244
- isUpdating = true;
4050
+ if (!anchor.parentNode) return;
4051
+ let node = null;
4052
+ let fromCache = false;
4053
+ if (shouldCache && cache.has(cacheKey)) {
4054
+ node = cache.get(cacheKey);
4055
+ fromCache = true;
4056
+ } else {
4057
+ try {
4058
+ const plan = await router2.loadPlan(routeDef, route2.path);
4059
+ if (kaTorn || seq !== updateSeq) return;
4060
+ node = await router2.instantiateComponent(plan, routeDef, route2.path);
4061
+ } catch (error) {
4062
+ if (kaTorn || seq !== updateSeq) return;
4063
+ console.error("[KeepAliveRoute] Component error:", error);
4064
+ return;
4065
+ }
4066
+ if (!node) return;
4067
+ if (kaTorn || seq !== updateSeq) {
4068
+ dispose(node);
4069
+ return;
4070
+ }
4071
+ }
3245
4072
  const parent = anchor.parentNode;
3246
4073
  if (!parent) {
3247
- isUpdating = false;
4074
+ if (!fromCache) dispose(node);
3248
4075
  return;
3249
4076
  }
3250
- try {
3251
- if (currentNode?.parentNode) {
3252
- parent.removeChild(currentNode);
3253
- if (!currentCached) {
3254
- dispose(currentNode);
3255
- }
3256
- }
3257
- if (shouldCache && cache.has(cacheKey)) {
3258
- currentNode = cache.get(cacheKey);
3259
- currentCached = true;
3260
- const idx = lruOrder.indexOf(cacheKey);
3261
- if (idx !== -1) {
3262
- lruOrder.splice(idx, 1);
3263
- }
3264
- lruOrder.push(cacheKey);
3265
- } else {
3266
- const component = await _routerRef.current.loadComponent(routeDef, route2.path);
3267
- const node = component();
3268
- if (!node || route2.path !== _routerRef.current.currentRoute.path) {
3269
- isUpdating = false;
3270
- return;
4077
+ if (currentNode && currentNode !== node) {
4078
+ if (currentNode.parentNode) currentNode.parentNode.removeChild(currentNode);
4079
+ if (!currentCached) dispose(currentNode);
4080
+ }
4081
+ if (fromCache) {
4082
+ const idx = lruOrder.indexOf(cacheKey);
4083
+ if (idx !== -1) lruOrder.splice(idx, 1);
4084
+ lruOrder.push(cacheKey);
4085
+ } else if (shouldCache) {
4086
+ cache.set(cacheKey, node);
4087
+ lruOrder.push(cacheKey);
4088
+ while (lruOrder.length > maxCache) {
4089
+ const evictKey = lruOrder.shift();
4090
+ if (evictKey === cacheKey) {
4091
+ lruOrder.push(evictKey);
4092
+ break;
3271
4093
  }
3272
- currentNode = node;
3273
- currentCached = shouldCache;
3274
- if (shouldCache) {
3275
- cache.set(cacheKey, node);
3276
- lruOrder.push(cacheKey);
3277
- while (lruOrder.length > maxCache) {
3278
- const evictKey = lruOrder.shift();
3279
- const evictNode = cache.get(evictKey);
3280
- if (evictNode) {
3281
- dispose(evictNode);
3282
- if (evictNode.parentNode) evictNode.parentNode.removeChild(evictNode);
3283
- cache.delete(evictKey);
3284
- }
3285
- }
4094
+ const evictNode = cache.get(evictKey);
4095
+ if (evictNode) {
4096
+ dispose(evictNode);
4097
+ if (evictNode.parentNode) evictNode.parentNode.removeChild(evictNode);
4098
+ cache.delete(evictKey);
3286
4099
  }
3287
4100
  }
3288
- currentKey = cacheKey;
3289
- if (currentNode) {
3290
- parent.insertBefore(currentNode, anchor.nextSibling);
3291
- }
3292
- } catch (error) {
3293
- console.error("[KeepAliveRoute] Component error:", error);
3294
- } finally {
3295
- isUpdating = false;
3296
- if (pendingUpdate) {
3297
- pendingUpdate = false;
3298
- update();
3299
- }
3300
4101
  }
4102
+ currentNode = node;
4103
+ currentCached = fromCache || shouldCache;
4104
+ currentKey = cacheKey;
4105
+ parent.insertBefore(node, anchor.nextSibling);
3301
4106
  };
3302
4107
  let initialized = false;
3303
4108
  const wrappedUpdate = async () => {
@@ -3310,10 +4115,10 @@ function KeepAliveRoute(options) {
3310
4115
  if (!initialized && anchor.parentNode) wrappedUpdate();
3311
4116
  });
3312
4117
  }
3313
- let kaTorn = false;
3314
4118
  const kaCleanup = () => {
3315
4119
  if (kaTorn) return;
3316
4120
  kaTorn = true;
4121
+ updateSeq++;
3317
4122
  kaTeardown();
3318
4123
  for (const node of cache.values()) {
3319
4124
  dispose(node);
@@ -3322,7 +4127,10 @@ function KeepAliveRoute(options) {
3322
4127
  cache.clear();
3323
4128
  lruOrder.length = 0;
3324
4129
  if (currentNode?.parentNode) currentNode.parentNode.removeChild(currentNode);
4130
+ if (currentNode && !currentCached) dispose(currentNode);
3325
4131
  currentNode = null;
4132
+ currentKey = "";
4133
+ currentCached = false;
3326
4134
  };
3327
4135
  registerDisposer(anchor, kaCleanup);
3328
4136
  routeCleanups.push(kaCleanup);
@@ -3335,8 +4143,12 @@ function RouterLink(props, children) {
3335
4143
  const baseClass = typeof classAttr === "string" ? classAttr : "";
3336
4144
  const routeGetter = _routerRef.current.routeGetter;
3337
4145
  const rawHref = _routerRef.current["resolvePath"](to);
3338
- const href = isSafeNavigationTarget(rawHref) ? rawHref : "#";
3339
- const hrefPath = href.split("?")[0].split("#")[0];
4146
+ const kind = classifyNavigationTarget(rawHref);
4147
+ const href = kind === "unsafe" ? "#" : rawHref;
4148
+ const [hrefBeforeHash, hrefHash = ""] = href.split("#");
4149
+ const [hrefPathRaw, hrefQueryRaw = ""] = hrefBeforeHash.split("?");
4150
+ const targetPath = normalizePathname(hrefPathRaw);
4151
+ const targetQuery = normalizeQuery(hrefQueryRaw);
3340
4152
  const link2 = document.createElement("a");
3341
4153
  link2.href = href;
3342
4154
  if (target) {
@@ -3352,8 +4164,9 @@ function RouterLink(props, children) {
3352
4164
  const options = _routerRef.current["options"];
3353
4165
  const effectCleanup = effect(() => {
3354
4166
  const route2 = routeGetter();
3355
- const isActive = route2.path.startsWith(hrefPath);
3356
- const isExactActive = route2.path === hrefPath;
4167
+ const currentPath = normalizePathname(route2.path);
4168
+ const isActive = kind === "internal" && isPathnameAncestor(targetPath, currentPath);
4169
+ const isExactActive = kind === "internal" && currentPath === targetPath && normalizeQuery(route2.query) === targetQuery && route2.hash === hrefHash;
3357
4170
  const classes = [];
3358
4171
  if (isActive) {
3359
4172
  if (activeClass) classes.push(activeClass);
@@ -3375,7 +4188,7 @@ function RouterLink(props, children) {
3375
4188
  const safe = sanitizeUrl(str);
3376
4189
  if (safe) link2.setAttribute(key, safe);
3377
4190
  } else if (lkey === "style") {
3378
- link2.setAttribute(key, sanitizeCSSValue(str));
4191
+ link2.setAttribute(key, sanitizeStyleAttribute(str));
3379
4192
  } else {
3380
4193
  link2.setAttribute(key, str);
3381
4194
  }
@@ -3395,10 +4208,13 @@ function RouterLink(props, children) {
3395
4208
  });
3396
4209
  }
3397
4210
  const onLinkClick = (e) => {
3398
- if (target || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) {
4211
+ if (e.defaultPrevented || target || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) {
3399
4212
  return;
3400
4213
  }
4214
+ if (link2.hasAttribute("download")) return;
4215
+ if (kind === "external") return;
3401
4216
  e.preventDefault();
4217
+ if (kind === "unsafe") return;
3402
4218
  _routerRef.current?.navigate(to, { replace: replace2 }).catch((err) => {
3403
4219
  if (typeof console !== "undefined") console.error("[router] link navigate failed:", err);
3404
4220
  });
@@ -3414,68 +4230,80 @@ function Suspense(props) {
3414
4230
  let currentNode = null;
3415
4231
  let fallbackNode = null;
3416
4232
  let isLoading = false;
4233
+ let generation = 0;
4234
+ let tornDown = false;
4235
+ const release = (node) => {
4236
+ if (!node) return;
4237
+ dispose(node);
4238
+ node.parentNode?.removeChild(node);
4239
+ };
4240
+ const commitTarget = (myGeneration) => tornDown || myGeneration !== generation ? null : anchor.parentNode;
3417
4241
  const cleanupNodes = () => {
3418
- [currentNode, fallbackNode].forEach((node) => {
3419
- if (node?.parentNode) {
3420
- node.parentNode.removeChild(node);
3421
- }
3422
- });
4242
+ release(currentNode);
4243
+ release(fallbackNode);
3423
4244
  currentNode = null;
3424
4245
  fallbackNode = null;
3425
4246
  };
3426
- const showFallback = () => {
3427
- if (fallbackNode || !props.fallback || !anchor.parentNode) return;
4247
+ const showFallback = (myGeneration) => {
4248
+ if (fallbackNode || !props.fallback || tornDown) return;
4249
+ if (!anchor.parentNode) return;
4250
+ let fallback;
3428
4251
  try {
3429
- const fallback = typeof props.fallback === "function" ? props.fallback() : props.fallback;
3430
- if (fallback instanceof HTMLElement) {
3431
- fallbackNode = fallback;
3432
- anchor.parentNode.insertBefore(fallbackNode, anchor.nextSibling);
3433
- }
4252
+ fallback = typeof props.fallback === "function" ? props.fallback() : props.fallback;
3434
4253
  } catch (error) {
3435
4254
  console.error("[Suspense] Fallback error:", error);
4255
+ return;
3436
4256
  }
3437
- };
3438
- const hideFallback = () => {
3439
- if (fallbackNode?.parentNode) {
3440
- fallbackNode.parentNode.removeChild(fallbackNode);
3441
- fallbackNode = null;
4257
+ if (!(fallback instanceof HTMLElement)) return;
4258
+ const parent = commitTarget(myGeneration);
4259
+ if (!parent) {
4260
+ release(fallback);
4261
+ return;
3442
4262
  }
4263
+ fallbackNode = fallback;
4264
+ parent.insertBefore(fallbackNode, anchor.nextSibling);
3443
4265
  };
3444
4266
  const render = async () => {
3445
- if (isLoading) return;
4267
+ if (tornDown || isLoading) return;
3446
4268
  isLoading = true;
4269
+ const myGeneration = ++generation;
3447
4270
  try {
3448
4271
  const result = props.nodes();
4272
+ let element;
3449
4273
  if (result instanceof Promise) {
3450
- showFallback();
3451
- const element = await result;
3452
- if (anchor.parentNode) {
3453
- cleanupNodes();
3454
- anchor.parentNode.insertBefore(element, anchor.nextSibling);
3455
- currentNode = element;
3456
- }
4274
+ showFallback(myGeneration);
4275
+ element = await result;
3457
4276
  } else {
3458
- if (anchor.parentNode) {
3459
- cleanupNodes();
3460
- anchor.parentNode.insertBefore(result, anchor.nextSibling);
3461
- currentNode = result;
3462
- }
4277
+ element = result;
3463
4278
  }
3464
- } catch (error) {
3465
- hideFallback();
3466
- console.error("[Suspense] Nodes error:", error);
3467
- if (anchor.parentNode) {
3468
- const errorElement = document.createElement("div");
3469
- errorElement.className = "suspense-error";
3470
- errorElement.textContent = error instanceof Error ? error.message : "Failed to load";
4279
+ const parent = commitTarget(myGeneration);
4280
+ if (!parent) {
4281
+ release(element);
3471
4282
  cleanupNodes();
3472
- anchor.parentNode.insertBefore(errorElement, anchor.nextSibling);
3473
- currentNode = errorElement;
4283
+ return;
3474
4284
  }
4285
+ cleanupNodes();
4286
+ parent.insertBefore(element, anchor.nextSibling);
4287
+ currentNode = element;
4288
+ } catch (error) {
4289
+ console.error("[Suspense] Nodes error:", error);
4290
+ const parent = commitTarget(myGeneration);
4291
+ cleanupNodes();
4292
+ if (!parent) return;
4293
+ const errorElement = document.createElement("div");
4294
+ errorElement.className = "suspense-error";
4295
+ errorElement.textContent = error instanceof Error ? error.message : "Failed to load";
4296
+ parent.insertBefore(errorElement, anchor.nextSibling);
4297
+ currentNode = errorElement;
3475
4298
  } finally {
3476
4299
  isLoading = false;
3477
4300
  }
3478
4301
  };
4302
+ registerDisposer(anchor, () => {
4303
+ tornDown = true;
4304
+ generation++;
4305
+ cleanupNodes();
4306
+ });
3479
4307
  queueMicrotask(render);
3480
4308
  return anchor;
3481
4309
  }
@@ -3490,12 +4318,15 @@ async function preloadRoute(to) {
3490
4318
  const match = _routerRef.current["matcher"].match(path2.split("?")[0].split("#")[0]);
3491
4319
  if (match && "component" in match.route) {
3492
4320
  try {
3493
- await _routerRef.current.loadComponent(match.route, path2);
4321
+ await _routerRef.current.preloadPlan(match.route, path2);
3494
4322
  } catch (error) {
3495
4323
  console.warn("[Router] Preload failed:", error);
3496
4324
  }
3497
4325
  }
3498
4326
  }
4327
+ function __getNavigationEpoch() {
4328
+ return _routerRef.current?.navigationEpoch ?? -1;
4329
+ }
3499
4330
  function hasRoute(name) {
3500
4331
  if (!_routerRef.current) return false;
3501
4332
  return _routerRef.current["matcher"].findByName(name) !== null;
@@ -3537,6 +4368,7 @@ function Outlet() {
3537
4368
  let currentNode = null;
3538
4369
  let currentChild = null;
3539
4370
  let navSeq = 0;
4371
+ let outletTorn = false;
3540
4372
  const clearCurrent = () => {
3541
4373
  if (currentNode) {
3542
4374
  dispose(currentNode);
@@ -3545,8 +4377,14 @@ function Outlet() {
3545
4377
  }
3546
4378
  currentChild = null;
3547
4379
  };
4380
+ const commitTarget = (seq) => outletTorn || seq !== navSeq ? null : anchor.parentNode;
4381
+ const release = (node) => {
4382
+ if (!node) return;
4383
+ dispose(node);
4384
+ node.parentNode?.removeChild(node);
4385
+ };
3548
4386
  const update = async () => {
3549
- if (!_routerRef.current) return;
4387
+ if (outletTorn || !_routerRef.current) return;
3550
4388
  const seq = ++navSeq;
3551
4389
  const route2 = _routerRef.current.currentRoute;
3552
4390
  if (route2.matched.length < 2) {
@@ -3561,17 +4399,21 @@ function Outlet() {
3561
4399
  if (childRoute === currentChild && currentNode) return;
3562
4400
  try {
3563
4401
  const cacheKey = `${route2.path}\0${childRoute.path}`;
3564
- const component = await _routerRef.current.loadComponent(childRoute, cacheKey);
3565
- if (seq !== navSeq) return;
3566
- const node = component();
3567
- if (node && anchor.parentNode) {
3568
- clearCurrent();
3569
- anchor.parentNode.insertBefore(node, anchor.nextSibling);
3570
- currentNode = node;
3571
- currentChild = childRoute;
4402
+ const plan = await _routerRef.current.loadPlan(childRoute, cacheKey);
4403
+ if (!commitTarget(seq)) return;
4404
+ const node = await _routerRef.current.instantiateComponent(plan, childRoute, cacheKey);
4405
+ const parent = commitTarget(seq);
4406
+ if (!parent) {
4407
+ release(node);
4408
+ return;
3572
4409
  }
4410
+ if (!node) return;
4411
+ clearCurrent();
4412
+ parent.insertBefore(node, anchor.nextSibling);
4413
+ currentNode = node;
4414
+ currentChild = childRoute;
3573
4415
  } catch (error) {
3574
- if (seq !== navSeq) return;
4416
+ if (outletTorn || seq !== navSeq) return;
3575
4417
  console.error("[Outlet] Failed to render child route:", error);
3576
4418
  }
3577
4419
  };
@@ -3581,16 +4423,17 @@ function Outlet() {
3581
4423
  if (anchor.parentNode) update();
3582
4424
  });
3583
4425
  }
3584
- let outletTorn = false;
3585
4426
  const outletCleanup = () => {
3586
4427
  if (outletTorn) return;
3587
4428
  outletTorn = true;
4429
+ navSeq++;
3588
4430
  outletTeardown();
3589
4431
  if (currentNode) {
3590
4432
  dispose(currentNode);
3591
4433
  if (currentNode.parentNode) currentNode.parentNode.removeChild(currentNode);
3592
4434
  currentNode = null;
3593
4435
  }
4436
+ currentChild = null;
3594
4437
  };
3595
4438
  registerDisposer(anchor, outletCleanup);
3596
4439
  routeCleanups.push(outletCleanup);
@@ -3661,6 +4504,8 @@ function isUnsafeKey(key) {
3661
4504
  }
3662
4505
 
3663
4506
  // src/plugins/routerSSR.ts
4507
+ init_headEntry();
4508
+ init_sanitize();
3664
4509
  function safeDecode(raw) {
3665
4510
  try {
3666
4511
  return decodeURIComponent(raw);
@@ -3858,22 +4703,16 @@ function renderRouteToString(url, routes, _options) {
3858
4703
  };
3859
4704
  }
3860
4705
  function renderRouteToDocument(url, routes, options) {
3861
- const { html: html2, state } = renderRouteToString(url, routes, options);
4706
+ const { html: html2, state: state2 } = renderRouteToString(url, routes, options);
3862
4707
  const opts = options || {};
3863
- const metaTags = (opts.meta || []).filter((attrs) => !isDangerousMetaRefresh(attrs)).map((attrs) => {
3864
- const pairs = buildSafeAttrString(attrs);
3865
- return pairs ? `<meta ${pairs} />` : "";
3866
- }).filter(Boolean).join("\n ");
3867
- const linkTags = (opts.links || []).map((attrs) => {
3868
- const pairs = buildSafeAttrString(attrs);
3869
- return pairs ? `<link ${pairs} />` : "";
3870
- }).filter(Boolean).join("\n ");
4708
+ const metaTags = (opts.meta || []).map((attrs) => serializeHeadEntry("meta", attrs)).filter(Boolean).join("\n ");
4709
+ const linkTags = (opts.links || []).map((attrs) => serializeHeadEntry("link", attrs)).filter(Boolean).join("\n ");
3871
4710
  const scriptTags = (opts.scripts || []).map((src) => {
3872
- const safe = sanitizeUrlLocal(String(src));
4711
+ const safe = sanitizeUrl(String(src));
3873
4712
  if (!safe) return "";
3874
4713
  return `<script src="${escapeAttrLocal(safe)}"></script>`;
3875
4714
  }).filter(Boolean).join("\n ");
3876
- const stateScript = serializeRouteState(state, opts.nonce);
4715
+ const stateScript = serializeRouteState(state2, opts.nonce);
3877
4716
  return `<!DOCTYPE html>
3878
4717
  <html>
3879
4718
  <head>
@@ -3891,8 +4730,8 @@ function renderRouteToDocument(url, routes, options) {
3891
4730
  </body>
3892
4731
  </html>`;
3893
4732
  }
3894
- function serializeRouteState(state, nonce) {
3895
- const json = escapeScriptJson(JSON.stringify(state));
4733
+ function serializeRouteState(state2, nonce) {
4734
+ const json = escapeScriptJson(JSON.stringify(state2));
3896
4735
  const nonceAttr = nonce ? ` nonce="${escapeAttrLocal(nonce)}"` : "";
3897
4736
  return `<script${nonceAttr}>window.${SSR_ROUTE_STATE_KEY}=${json}</script>`;
3898
4737
  }
@@ -3908,20 +4747,23 @@ function hydrateRouter(routes, options) {
3908
4747
  }
3909
4748
  createRouter(routes);
3910
4749
  const container = options?.container || document.getElementById("app");
3911
- if (container && serverState.path) {
3912
- const resolved = resolveServerRoute(serverState.path, routes);
4750
+ if (!container) return;
4751
+ const liveUrl = `${window.location.pathname}${window.location.search}${window.location.hash}`;
4752
+ const resolved = resolveServerRoute(liveUrl, routes);
4753
+ const bootstrapEpoch = __getNavigationEpoch();
4754
+ Promise.resolve().then(() => (init_ssr(), ssr_exports)).then(async ({ hydrate: hydrate2 }) => {
4755
+ const { replaceChildrenSafely: replaceChildrenSafely2 } = await Promise.resolve().then(() => (init_dispose(), dispose_exports));
4756
+ if (__getNavigationEpoch() !== bootstrapEpoch) return;
3913
4757
  if (resolved.component) {
3914
- Promise.resolve().then(() => (init_ssr(), ssr_exports)).then(({ hydrate: hydrate2 }) => {
3915
- if (resolved.component) {
3916
- hydrate2(resolved.component, container);
3917
- }
3918
- }).catch((err) => {
3919
- if (typeof console !== "undefined") {
3920
- console.error("[SibuJS routerSSR] failed to load hydrate:", err);
3921
- }
3922
- });
4758
+ hydrate2(resolved.component, container);
4759
+ return;
3923
4760
  }
3924
- }
4761
+ replaceChildrenSafely2(container);
4762
+ }).catch((err) => {
4763
+ if (typeof console !== "undefined") {
4764
+ console.error("[SibuJS routerSSR] failed to load hydrate:", err);
4765
+ }
4766
+ });
3925
4767
  }
3926
4768
  function createSSRRouter(routes) {
3927
4769
  return {
@@ -3936,54 +4778,6 @@ function createSSRRouter(routes) {
3936
4778
  }
3937
4779
  };
3938
4780
  }
3939
- var SAFE_ATTR_NAME2 = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
3940
- function isSafeAttrName2(name) {
3941
- return SAFE_ATTR_NAME2.test(name);
3942
- }
3943
- function isEventHandlerAttr2(name) {
3944
- if (name.length < 3) return false;
3945
- const lower = name.toLowerCase();
3946
- return lower[0] === "o" && lower[1] === "n" && lower.charCodeAt(2) >= 97 && lower.charCodeAt(2) <= 122;
3947
- }
3948
- var URL_ATTRS2 = /* @__PURE__ */ new Set([
3949
- "href",
3950
- "src",
3951
- "action",
3952
- "formaction",
3953
- "cite",
3954
- "poster",
3955
- "background",
3956
- "srcset",
3957
- "ping",
3958
- "manifest",
3959
- "data",
3960
- "xlink:href"
3961
- ]);
3962
- function sanitizeUrlLocal(url) {
3963
- const trimmed = url.replace(/[\x00-\x20\x7f-\x9f]+/g, "").trim();
3964
- if (!trimmed) return "";
3965
- const lower = trimmed.toLowerCase();
3966
- if (lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("vbscript:") || lower.startsWith("blob:")) {
3967
- return "";
3968
- }
3969
- return trimmed;
3970
- }
3971
- function buildSafeAttrString(attrs) {
3972
- const out = [];
3973
- for (const rawKey of Object.keys(attrs)) {
3974
- if (!Object.hasOwn(attrs, rawKey)) continue;
3975
- if (!isSafeAttrName2(rawKey)) continue;
3976
- if (isEventHandlerAttr2(rawKey)) continue;
3977
- const lowerKey = rawKey.toLowerCase();
3978
- let value = String(attrs[rawKey]);
3979
- if (URL_ATTRS2.has(lowerKey)) {
3980
- value = sanitizeUrlLocal(value);
3981
- if (!value) continue;
3982
- }
3983
- out.push(`${rawKey}="${escapeAttrLocal(value)}"`);
3984
- }
3985
- return out.join(" ");
3986
- }
3987
4781
  function escapeHtmlLocal(str) {
3988
4782
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3989
4783
  }
@@ -4816,6 +5610,7 @@ function createBootSequence() {
4816
5610
  Suspense,
4817
5611
  Trans,
4818
5612
  VERSION,
5613
+ __getNavigationEpoch,
4819
5614
  __removeRouterPagehideHandler,
4820
5615
  addRoute,
4821
5616
  afterEach,