sibujs 3.1.0 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +6 -0
  2. package/dist/browser.cjs +16 -8
  3. package/dist/browser.js +6 -5
  4. package/dist/build.cjs +235 -147
  5. package/dist/build.js +35 -24
  6. package/dist/cdn.global.js +7 -7
  7. package/dist/{chunk-WYU7CYJ3.js → chunk-2C4E3HBM.js} +5 -5
  8. package/dist/{chunk-3DYB5B3S.js → chunk-4JCAUOLN.js} +45 -23
  9. package/dist/{chunk-2HAGQWDV.js → chunk-5N74TKLD.js} +1 -1
  10. package/dist/{chunk-SVVAUX7J.js → chunk-7XDYVJLE.js} +19 -9
  11. package/dist/{chunk-2N2UL7O4.js → chunk-BGNLPNGV.js} +20 -12
  12. package/dist/{chunk-RK4BQG25.js → chunk-C427DVQF.js} +1 -1
  13. package/dist/{chunk-ZIBE2SAT.js → chunk-FDY42FIU.js} +3 -2
  14. package/dist/{chunk-GQ7RRFPU.js → chunk-FOI23UJL.js} +11 -1
  15. package/dist/{chunk-2RA7SHDA.js → chunk-GOJMFRBL.js} +20 -4
  16. package/dist/{chunk-IVOUCSZL.js → chunk-GOUM4JCT.js} +6 -6
  17. package/dist/chunk-H3SRKIYX.js +17 -0
  18. package/dist/{chunk-3DJH25UO.js → chunk-H6PCHJZQ.js} +2 -2
  19. package/dist/{chunk-UCS6AMJ7.js → chunk-HMJFCBRR.js} +26 -3
  20. package/dist/{chunk-JYD2PWXH.js → chunk-HXMS4SNP.js} +22 -15
  21. package/dist/{chunk-SC437AMI.js → chunk-JYXOEYI4.js} +12 -18
  22. package/dist/{chunk-KB3BA2XK.js → chunk-NFYWLRUO.js} +11 -18
  23. package/dist/{chunk-QNQY5DUS.js → chunk-NPIEEKPT.js} +20 -11
  24. package/dist/{chunk-UYX2NDOH.js → chunk-OYLPZO4N.js} +33 -15
  25. package/dist/{chunk-LYTCUZ7H.js → chunk-RDRSWYNP.js} +1 -1
  26. package/dist/{chunk-2ZJ7TSW4.js → chunk-RLUJL2MV.js} +4 -8
  27. package/dist/{chunk-CR4MXPHB.js → chunk-V2MTG5FT.js} +99 -36
  28. package/dist/{chunk-CNZ35WI2.js → chunk-VJE6DDYM.js} +2 -2
  29. package/dist/{chunk-PMSDFTK3.js → chunk-VOCE4NNK.js} +157 -75
  30. package/dist/{chunk-WKUXSE7V.js → chunk-X67UYC74.js} +12 -11
  31. package/dist/{chunk-EFOAE5NC.js → chunk-YFDGQWDA.js} +1 -1
  32. package/dist/{chunk-3U4ZVXVD.js → chunk-Z2FWAE4B.js} +6 -2
  33. package/dist/data.cjs +190 -94
  34. package/dist/data.d.cts +7 -1
  35. package/dist/data.d.ts +7 -1
  36. package/dist/data.js +8 -8
  37. package/dist/devtools.cjs +38 -10
  38. package/dist/devtools.d.cts +1 -1
  39. package/dist/devtools.d.ts +1 -1
  40. package/dist/devtools.js +6 -6
  41. package/dist/ecosystem.cjs +123 -63
  42. package/dist/ecosystem.js +9 -9
  43. package/dist/extras.cjs +380 -196
  44. package/dist/extras.d.cts +2 -2
  45. package/dist/extras.d.ts +2 -2
  46. package/dist/extras.js +27 -24
  47. package/dist/index.cjs +214 -136
  48. package/dist/index.d.cts +15 -2
  49. package/dist/index.d.ts +15 -2
  50. package/dist/index.js +15 -13
  51. package/dist/{introspect-BZWKvQUZ.d.ts → introspect-DOZfmC-4.d.ts} +1 -1
  52. package/dist/{introspect-DsJlDD2T.d.cts → introspect-RjLfIFpL.d.cts} +1 -1
  53. package/dist/motion.cjs +10 -0
  54. package/dist/motion.js +3 -3
  55. package/dist/patterns.cjs +45 -40
  56. package/dist/patterns.js +8 -7
  57. package/dist/performance.cjs +101 -25
  58. package/dist/performance.d.cts +2 -2
  59. package/dist/performance.d.ts +2 -2
  60. package/dist/performance.js +8 -7
  61. package/dist/plugins.cjs +234 -160
  62. package/dist/plugins.d.cts +1 -1
  63. package/dist/plugins.d.ts +1 -1
  64. package/dist/plugins.js +127 -69
  65. package/dist/{ssr-FXD2PPMC.js → ssr-2QDQ27EV.js} +5 -3
  66. package/dist/{ssr-CrVNy6Pa.d.cts → ssr-D62yFwuw.d.cts} +8 -1
  67. package/dist/{ssr-CrVNy6Pa.d.ts → ssr-D62yFwuw.d.ts} +8 -1
  68. package/dist/ssr.cjs +145 -66
  69. package/dist/ssr.d.cts +1 -1
  70. package/dist/ssr.d.ts +1 -1
  71. package/dist/ssr.js +12 -10
  72. package/dist/testing.cjs +9 -4
  73. package/dist/testing.js +3 -3
  74. package/dist/ui.cjs +54 -38
  75. package/dist/ui.js +10 -9
  76. package/dist/widgets.cjs +40 -24
  77. package/dist/widgets.js +8 -8
  78. package/package.json +3 -1
package/dist/plugins.cjs CHANGED
@@ -43,8 +43,16 @@ var init_dev = __esm({
43
43
  });
44
44
 
45
45
  // src/utils/sanitize.ts
46
+ function stripControlChars(value) {
47
+ return value.replace(/[\x00-\x20\x7f-\x9f]+/g, "");
48
+ }
49
+ function isEventHandlerAttr(name) {
50
+ if (name.length < 3) return false;
51
+ const lower = name.toLowerCase();
52
+ return lower[0] === "o" && lower[1] === "n" && lower.charCodeAt(2) >= 97 && lower.charCodeAt(2) <= 122;
53
+ }
46
54
  function sanitizeUrl(url) {
47
- const trimmed = url.replace(/[\x00-\x20\x7f-\x9f]+/g, "").trim();
55
+ const trimmed = stripControlChars(url).trim();
48
56
  if (!trimmed) return "";
49
57
  const lower = trimmed.toLowerCase();
50
58
  let schemeEnd = -1;
@@ -93,7 +101,7 @@ function sanitizeCSSValue(value) {
93
101
  return value;
94
102
  }
95
103
  function isUrlAttribute(attr) {
96
- return URL_ATTRIBUTES.has(attr);
104
+ return URL_ATTRIBUTES.has(attr.toLowerCase());
97
105
  }
98
106
  var SAFE_URL_PROTOCOLS, URL_ATTRIBUTES;
99
107
  var init_sanitize = __esm({
@@ -135,11 +143,15 @@ var init_ssr_context = __esm({
135
143
  als = null;
136
144
  try {
137
145
  if (typeof process !== "undefined" && process.versions && process.versions.node) {
138
- const req = Function("return typeof require==='function'?require:null")();
139
- if (req) {
140
- const mod = req("node:async_hooks");
141
- als = new mod.AsyncLocalStorage();
146
+ let mod = null;
147
+ const getBuiltin = process.getBuiltinModule;
148
+ if (typeof getBuiltin === "function") {
149
+ mod = getBuiltin("node:async_hooks");
150
+ } else {
151
+ const req = Function("return typeof require==='function'?require:null")();
152
+ if (req) mod = req("node:async_hooks");
142
153
  }
154
+ if (mod) als = new mod.AsyncLocalStorage();
143
155
  }
144
156
  } catch {
145
157
  als = null;
@@ -157,6 +169,7 @@ __export(ssr_exports, {
157
169
  hydrate: () => hydrate,
158
170
  hydrateIslands: () => hydrateIslands,
159
171
  hydrateProgressively: () => hydrateProgressively,
172
+ isDangerousMetaRefresh: () => isDangerousMetaRefresh,
160
173
  island: () => island,
161
174
  renderToDocument: () => renderToDocument,
162
175
  renderToReadableStream: () => renderToReadableStream,
@@ -169,14 +182,12 @@ __export(ssr_exports, {
169
182
  suspenseSwapScript: () => suspenseSwapScript,
170
183
  trustHTML: () => trustHTML
171
184
  });
185
+ function sanitizeUrlAttr(name, value) {
186
+ return name === "srcset" ? sanitizeSrcset(value) : sanitizeUrl(value);
187
+ }
172
188
  function isSafeAttrName(name) {
173
189
  return SAFE_ATTR_NAME.test(name);
174
190
  }
175
- function isEventHandlerAttr2(name) {
176
- if (name.length < 3) return false;
177
- const lower = name.toLowerCase();
178
- return lower[0] === "o" && lower[1] === "n" && lower.charCodeAt(2) >= 97 && lower.charCodeAt(2) <= 122;
179
- }
180
191
  function ssrErrorComment(err) {
181
192
  if (_isDev8) {
182
193
  const msg = escapeHtml(err instanceof Error ? err.message : String(err));
@@ -217,11 +228,11 @@ function renderToString(element) {
217
228
  for (const attr of Array.from(element.attributes)) {
218
229
  const rawName = attr.name;
219
230
  if (!isSafeAttrName(rawName)) continue;
220
- if (isEventHandlerAttr2(rawName)) continue;
231
+ if (isEventHandlerAttr(rawName)) continue;
221
232
  const lowerName = rawName.toLowerCase();
222
233
  let value = attr.value;
223
234
  if (URL_ATTRS.has(lowerName)) {
224
- value = sanitizeUrl(value);
235
+ value = sanitizeUrlAttr(lowerName, value);
225
236
  if (!value) continue;
226
237
  }
227
238
  html2 += ` ${rawName}="${escapeAttr(value)}"`;
@@ -358,11 +369,11 @@ function buildAttrString(attrs, { allowEventHandlers = false } = {}) {
358
369
  for (const rawKey of Object.keys(attrs)) {
359
370
  if (!Object.hasOwn(attrs, rawKey)) continue;
360
371
  if (!isSafeAttrName(rawKey)) continue;
361
- if (!allowEventHandlers && isEventHandlerAttr2(rawKey)) continue;
372
+ if (!allowEventHandlers && isEventHandlerAttr(rawKey)) continue;
362
373
  const lowerKey = rawKey.toLowerCase();
363
374
  let value = String(attrs[rawKey]);
364
375
  if (URL_ATTRS.has(lowerKey)) {
365
- value = sanitizeUrl(value);
376
+ value = sanitizeUrlAttr(lowerKey, value);
366
377
  if (!value) continue;
367
378
  }
368
379
  out.push(`${rawKey}="${escapeAttr(value)}"`);
@@ -370,12 +381,17 @@ function buildAttrString(attrs, { allowEventHandlers = false } = {}) {
370
381
  return out.join(" ");
371
382
  }
372
383
  function isDangerousMetaRefresh(metaProps) {
373
- const httpEquiv = metaProps["http-equiv"];
384
+ let httpEquiv;
385
+ let content;
386
+ for (const k in metaProps) {
387
+ const lk = k.toLowerCase();
388
+ if (lk === "http-equiv") httpEquiv = metaProps[k];
389
+ else if (lk === "content") content = metaProps[k];
390
+ }
374
391
  if (typeof httpEquiv !== "string") return false;
375
392
  if (httpEquiv.toLowerCase() !== "refresh") return false;
376
- const content = metaProps.content;
377
393
  if (typeof content !== "string") return false;
378
- const normalized = content.replace(/[\x00-\x20\x7f-\x9f]+/g, "").toLowerCase();
394
+ const normalized = stripControlChars(content).toLowerCase();
379
395
  return normalized.includes("url=javascript:") || normalized.includes("url=data:") || normalized.includes("url=vbscript:") || normalized.includes("url=blob:");
380
396
  }
381
397
  function renderToDocument(component, options = {}) {
@@ -453,11 +469,11 @@ async function* renderToStream(element) {
453
469
  for (const attr of Array.from(element.attributes)) {
454
470
  const rawName = attr.name;
455
471
  if (!isSafeAttrName(rawName)) continue;
456
- if (isEventHandlerAttr2(rawName)) continue;
472
+ if (isEventHandlerAttr(rawName)) continue;
457
473
  const lowerName = rawName.toLowerCase();
458
474
  let value = attr.value;
459
475
  if (URL_ATTRS.has(lowerName)) {
460
- value = sanitizeUrl(value);
476
+ value = sanitizeUrlAttr(lowerName, value);
461
477
  if (!value) continue;
462
478
  }
463
479
  openTag += ` ${rawName}="${escapeAttr(value)}"`;
@@ -533,11 +549,11 @@ function hydrateProgressively(container, islands, options) {
533
549
  (entries) => {
534
550
  for (const entry of entries) {
535
551
  if (entry.isIntersecting) {
552
+ observer.disconnect();
536
553
  const clientTree = factory();
537
554
  clientTree.setAttribute("data-sibu-island", id);
538
555
  clientTree.setAttribute("data-sibu-hydrated", "true");
539
556
  marker2.replaceWith(clientTree);
540
- observer.disconnect();
541
557
  break;
542
558
  }
543
559
  }
@@ -922,7 +938,7 @@ function track(effectFn, subscriber) {
922
938
  function reactiveBinding(commit) {
923
939
  const run = () => {
924
940
  const s2 = subscriber;
925
- if (s2._reentrant) return;
941
+ if (s2._disposed || s2._reentrant) return;
926
942
  s2._reentrant = true;
927
943
  try {
928
944
  retrack(commit, subscriber);
@@ -938,8 +954,12 @@ function reactiveBinding(commit) {
938
954
  subscriber._runEpoch = 0;
939
955
  subscriber._runs = 0;
940
956
  subscriber._reentrant = false;
957
+ subscriber._disposed = false;
941
958
  run();
942
- return subscriber._dispose ?? (subscriber._dispose = () => cleanup(subscriber));
959
+ return subscriber._dispose ?? (subscriber._dispose = () => {
960
+ subscriber._disposed = true;
961
+ cleanup(subscriber);
962
+ });
943
963
  }
944
964
  function recordDependency(signal2) {
945
965
  if (!currentSubscriber) return;
@@ -1097,11 +1117,6 @@ var _isDev3 = isDev();
1097
1117
  function setProp(el, key, val) {
1098
1118
  el[key] = val;
1099
1119
  }
1100
- function isEventHandlerAttr(name) {
1101
- if (name.length < 3) return false;
1102
- const lower = name.toLowerCase();
1103
- return lower[0] === "o" && lower[1] === "n" && lower.charCodeAt(2) >= 97 && lower.charCodeAt(2) <= 122;
1104
- }
1105
1120
  function bindAttribute(el, attr, getter) {
1106
1121
  if (isEventHandlerAttr(attr)) {
1107
1122
  if (_isDev3)
@@ -1142,7 +1157,71 @@ function bindAttribute(el, attr, getter) {
1142
1157
 
1143
1158
  // src/reactivity/bindChildNode.ts
1144
1159
  init_dev();
1160
+
1161
+ // src/core/rendering/dispose.ts
1162
+ init_dev();
1163
+ var elementDisposers = /* @__PURE__ */ new WeakMap();
1145
1164
  var _isDev4 = isDev();
1165
+ var activeBindingCount = 0;
1166
+ function registerDisposer(node, teardown) {
1167
+ let disposers = elementDisposers.get(node);
1168
+ if (!disposers) {
1169
+ disposers = [];
1170
+ elementDisposers.set(node, disposers);
1171
+ }
1172
+ disposers.push(teardown);
1173
+ if (_isDev4) activeBindingCount++;
1174
+ }
1175
+ function dispose(node) {
1176
+ const stack = [node];
1177
+ const order = [];
1178
+ while (stack.length > 0) {
1179
+ const current = stack.pop();
1180
+ order.push(current);
1181
+ const children = Array.from(current.childNodes);
1182
+ for (let i2 = 0; i2 < children.length; i2++) {
1183
+ stack.push(children[i2]);
1184
+ }
1185
+ }
1186
+ for (let i2 = order.length - 1; i2 >= 0; i2--) {
1187
+ const current = order[i2];
1188
+ const disposers = elementDisposers.get(current);
1189
+ if (disposers) {
1190
+ const snapshot = disposers.slice();
1191
+ elementDisposers.delete(current);
1192
+ if (_isDev4) activeBindingCount -= snapshot.length;
1193
+ for (const d of snapshot) {
1194
+ try {
1195
+ d();
1196
+ } catch (err) {
1197
+ if (_isDev4 && typeof console !== "undefined") {
1198
+ console.warn("[SibuJS] Disposer threw during cleanup:", err);
1199
+ }
1200
+ }
1201
+ }
1202
+ let extraPasses = 0;
1203
+ while (extraPasses++ < 8) {
1204
+ const added = elementDisposers.get(current);
1205
+ if (!added || added.length === 0) break;
1206
+ const moreSnapshot = added.slice();
1207
+ elementDisposers.delete(current);
1208
+ if (_isDev4) activeBindingCount -= moreSnapshot.length;
1209
+ for (const d of moreSnapshot) {
1210
+ try {
1211
+ d();
1212
+ } catch (err) {
1213
+ if (_isDev4 && typeof console !== "undefined") {
1214
+ console.warn("[SibuJS] Disposer threw during cleanup:", err);
1215
+ }
1216
+ }
1217
+ }
1218
+ }
1219
+ }
1220
+ }
1221
+ }
1222
+
1223
+ // src/reactivity/bindChildNode.ts
1224
+ var _isDev5 = isDev();
1146
1225
  function bindChildNode(placeholder, getter) {
1147
1226
  let lastNodes = [];
1148
1227
  function commit() {
@@ -1150,12 +1229,13 @@ function bindChildNode(placeholder, getter) {
1150
1229
  try {
1151
1230
  result = getter();
1152
1231
  } catch (err) {
1153
- if (_isDev4) devWarn(`bindChildNode: getter threw: ${err instanceof Error ? err.message : String(err)}`);
1232
+ if (_isDev5) devWarn(`bindChildNode: getter threw: ${err instanceof Error ? err.message : String(err)}`);
1154
1233
  return;
1155
1234
  }
1156
1235
  if (result == null || typeof result === "boolean") {
1157
1236
  for (let i2 = 0; i2 < lastNodes.length; i2++) {
1158
1237
  const node = lastNodes[i2];
1238
+ dispose(node);
1159
1239
  if (node.parentNode) node.parentNode.removeChild(node);
1160
1240
  }
1161
1241
  lastNodes.length = 0;
@@ -1175,7 +1255,7 @@ function bindChildNode(placeholder, getter) {
1175
1255
  if (item == null || typeof item === "boolean") continue;
1176
1256
  const node = item instanceof Node ? item : document.createTextNode(String(item));
1177
1257
  if (seen.has(node)) {
1178
- if (_isDev4)
1258
+ if (_isDev5)
1179
1259
  devWarn("bindChildNode: duplicate node reference in array \u2014 only the first occurrence is rendered.");
1180
1260
  continue;
1181
1261
  }
@@ -1197,18 +1277,16 @@ function bindChildNode(placeholder, getter) {
1197
1277
  for (let i2 = 0; i2 < lastNodes.length; i2++) {
1198
1278
  const node = lastNodes[i2];
1199
1279
  if (reused?.has(node)) continue;
1280
+ dispose(node);
1200
1281
  if (node.parentNode) node.parentNode.removeChild(node);
1201
1282
  }
1202
- const anchor = placeholder.nextSibling;
1283
+ let prev = placeholder;
1203
1284
  for (let i2 = 0; i2 < newNodes.length; i2++) {
1204
1285
  const node = newNodes[i2];
1205
- if (reused?.has(node) && node.parentNode === parent) {
1206
- if (node.nextSibling !== anchor) {
1207
- parent.insertBefore(node, anchor);
1208
- }
1209
- } else {
1210
- parent.insertBefore(node, anchor);
1286
+ if (prev.nextSibling !== node) {
1287
+ parent.insertBefore(node, prev.nextSibling);
1211
1288
  }
1289
+ prev = node;
1212
1290
  }
1213
1291
  lastNodes = newNodes;
1214
1292
  }
@@ -1217,70 +1295,6 @@ function bindChildNode(placeholder, getter) {
1217
1295
 
1218
1296
  // src/core/rendering/tagFactory.ts
1219
1297
  init_sanitize();
1220
-
1221
- // src/core/rendering/dispose.ts
1222
- init_dev();
1223
- var elementDisposers = /* @__PURE__ */ new WeakMap();
1224
- var _isDev5 = isDev();
1225
- var activeBindingCount = 0;
1226
- function registerDisposer(node, teardown) {
1227
- let disposers = elementDisposers.get(node);
1228
- if (!disposers) {
1229
- disposers = [];
1230
- elementDisposers.set(node, disposers);
1231
- }
1232
- disposers.push(teardown);
1233
- if (_isDev5) activeBindingCount++;
1234
- }
1235
- function dispose(node) {
1236
- const stack = [node];
1237
- const order = [];
1238
- while (stack.length > 0) {
1239
- const current = stack.pop();
1240
- order.push(current);
1241
- const children = Array.from(current.childNodes);
1242
- for (let i2 = 0; i2 < children.length; i2++) {
1243
- stack.push(children[i2]);
1244
- }
1245
- }
1246
- for (let i2 = order.length - 1; i2 >= 0; i2--) {
1247
- const current = order[i2];
1248
- const disposers = elementDisposers.get(current);
1249
- if (disposers) {
1250
- const snapshot = disposers.slice();
1251
- elementDisposers.delete(current);
1252
- if (_isDev5) activeBindingCount -= snapshot.length;
1253
- for (const d of snapshot) {
1254
- try {
1255
- d();
1256
- } catch (err) {
1257
- if (_isDev5 && typeof console !== "undefined") {
1258
- console.warn("[SibuJS] Disposer threw during cleanup:", err);
1259
- }
1260
- }
1261
- }
1262
- let extraPasses = 0;
1263
- while (extraPasses++ < 8) {
1264
- const added = elementDisposers.get(current);
1265
- if (!added || added.length === 0) break;
1266
- const moreSnapshot = added.slice();
1267
- elementDisposers.delete(current);
1268
- if (_isDev5) activeBindingCount -= moreSnapshot.length;
1269
- for (const d of moreSnapshot) {
1270
- try {
1271
- d();
1272
- } catch (err) {
1273
- if (_isDev5 && typeof console !== "undefined") {
1274
- console.warn("[SibuJS] Disposer threw during cleanup:", err);
1275
- }
1276
- }
1277
- }
1278
- }
1279
- }
1280
- }
1281
- }
1282
-
1283
- // src/core/rendering/tagFactory.ts
1284
1298
  var SVG_NS = "http://www.w3.org/2000/svg";
1285
1299
  var _isDev6 = isDev();
1286
1300
  var BLOCKED_TAGS = /* @__PURE__ */ new Set(["script", "iframe", "object", "embed", "frame", "frameset"]);
@@ -1515,7 +1529,7 @@ var tagFactory = (tag, ns) => {
1515
1529
  const value = props[key];
1516
1530
  if (value == null) continue;
1517
1531
  const lkey = key.toLowerCase();
1518
- if (lkey[0] === "o" && lkey[1] === "n") continue;
1532
+ if (isEventHandlerAttr(key)) continue;
1519
1533
  if (typeof value === "function") {
1520
1534
  registerDisposer(el, bindAttribute(el, key, value));
1521
1535
  } else if (typeof value === "boolean") {
@@ -1879,6 +1893,7 @@ function effect(effectFn, options) {
1879
1893
  ctx.fn(ctx.onCleanup);
1880
1894
  };
1881
1895
  const sub2 = (() => {
1896
+ if (ctx.disposed) return;
1882
1897
  if (ctx.running) {
1883
1898
  ctx.rerunPending = true;
1884
1899
  return;
@@ -1918,7 +1933,9 @@ function effect(effectFn, options) {
1918
1933
  init_sanitize();
1919
1934
  function isSafeNavigationTarget(path2) {
1920
1935
  if (path2 === "") return true;
1921
- return sanitizeUrl(path2) !== "";
1936
+ const normalized = stripControlChars(path2).replace(/\\/g, "/");
1937
+ if (normalized.startsWith("//")) return false;
1938
+ return sanitizeUrl(normalized) !== "";
1922
1939
  }
1923
1940
  var LRUCache = class {
1924
1941
  constructor(maxSize = 100) {
@@ -2055,8 +2072,13 @@ var RouteMatcher = class {
2055
2072
  if (match) {
2056
2073
  const params = {};
2057
2074
  compiled.keys.forEach((key, i2) => {
2058
- if (match[i2 + 1] !== void 0) {
2059
- params[key] = decodeURIComponent(match[i2 + 1]);
2075
+ const raw = match[i2 + 1];
2076
+ if (raw !== void 0) {
2077
+ try {
2078
+ params[key] = decodeURIComponent(raw);
2079
+ } catch {
2080
+ params[key] = raw;
2081
+ }
2060
2082
  }
2061
2083
  });
2062
2084
  return { params };
@@ -2107,14 +2129,23 @@ var RouteMatcher = class {
2107
2129
  }
2108
2130
  }
2109
2131
  removeRoute(path2) {
2110
- this.routeTrie.delete(path2);
2111
- this.parentChain.delete(path2);
2112
2132
  this.compiledPatterns.clear();
2113
- for (const [name, route2] of this.namedRoutes) {
2114
- if (route2.path === path2) {
2115
- this.namedRoutes.delete(name);
2116
- break;
2117
- }
2133
+ const root = this.routeTrie.get(path2);
2134
+ if (!root) return;
2135
+ const removed = /* @__PURE__ */ new Set();
2136
+ const collect = (r) => {
2137
+ removed.add(r);
2138
+ if (r.children) for (const child of r.children) collect(child);
2139
+ };
2140
+ collect(root);
2141
+ for (const [key, route2] of [...this.routeTrie]) {
2142
+ if (removed.has(route2)) this.routeTrie.delete(key);
2143
+ }
2144
+ for (const [key, chain] of [...this.parentChain]) {
2145
+ if (chain.length > 0 && removed.has(chain[chain.length - 1])) this.parentChain.delete(key);
2146
+ }
2147
+ for (const [name, route2] of [...this.namedRoutes]) {
2148
+ if (removed.has(route2)) this.namedRoutes.delete(name);
2118
2149
  }
2119
2150
  }
2120
2151
  };
@@ -2226,42 +2257,61 @@ var GuardManager = class {
2226
2257
  this.afterEachHooks = [];
2227
2258
  }
2228
2259
  };
2229
- var ComponentLoader = class {
2260
+ var _ComponentLoader = class _ComponentLoader {
2230
2261
  constructor(cacheSize = 50, retryDelay = 1e3) {
2231
2262
  this.errorCache = /* @__PURE__ */ new Map();
2232
2263
  this.loadingPromises = /* @__PURE__ */ new Map();
2264
+ // Stable per-route-definition id. Caching by the RESOLVED path (e.g.
2265
+ // /users/123) gave the cache one entry per visited URL, so parameterized
2266
+ // routes thrashed/evicted and the component was reloaded every navigation.
2267
+ // Keying by route-definition identity makes the cache effective again.
2268
+ this.routeKeys = /* @__PURE__ */ new WeakMap();
2269
+ this.keyCounter = 0;
2233
2270
  this.componentCache = new LRUCache(cacheSize);
2234
2271
  this.retryDelay = retryDelay;
2235
2272
  }
2273
+ keyFor(route2) {
2274
+ let key = this.routeKeys.get(route2);
2275
+ if (key === void 0) {
2276
+ key = `route#${this.keyCounter++}`;
2277
+ this.routeKeys.set(route2, key);
2278
+ }
2279
+ return key;
2280
+ }
2236
2281
  async loadComponent(route2, routePath) {
2237
2282
  if (!("component" in route2)) {
2238
2283
  throw new Error(`Route ${routePath} does not have a component`);
2239
2284
  }
2240
2285
  const comp = route2.component;
2241
- const cached = this.componentCache.get(routePath);
2286
+ const cacheKey = this.keyFor(route2);
2287
+ const cached = this.componentCache.get(cacheKey);
2242
2288
  if (cached) return cached;
2243
- const existingPromise = this.loadingPromises.get(routePath);
2289
+ const existingPromise = this.loadingPromises.get(cacheKey);
2244
2290
  if (existingPromise) return existingPromise;
2245
- const errorInfo = this.errorCache.get(routePath);
2291
+ const errorInfo = this.errorCache.get(cacheKey);
2246
2292
  if (errorInfo && Date.now() - errorInfo.timestamp < this.retryDelay) {
2247
2293
  throw new Error(`Component loading failed recently, retry in ${this.retryDelay}ms`);
2248
2294
  }
2249
2295
  const loadingPromise = this.doLoadComponent(comp, routePath);
2250
- this.loadingPromises.set(routePath, loadingPromise);
2296
+ this.loadingPromises.set(cacheKey, loadingPromise);
2251
2297
  try {
2252
2298
  const component = await loadingPromise;
2253
- this.componentCache.set(routePath, component);
2254
- this.errorCache.delete(routePath);
2299
+ this.componentCache.set(cacheKey, component);
2300
+ this.errorCache.delete(cacheKey);
2255
2301
  return component;
2256
2302
  } catch (error) {
2257
- const currentError = this.errorCache.get(routePath) || { timestamp: 0, count: 0 };
2258
- this.errorCache.set(routePath, {
2303
+ const currentError = this.errorCache.get(cacheKey) || { timestamp: 0, count: 0 };
2304
+ if (!this.errorCache.has(cacheKey) && this.errorCache.size >= _ComponentLoader.MAX_ERROR_ENTRIES) {
2305
+ const oldest = this.errorCache.keys().next().value;
2306
+ if (oldest !== void 0) this.errorCache.delete(oldest);
2307
+ }
2308
+ this.errorCache.set(cacheKey, {
2259
2309
  timestamp: Date.now(),
2260
2310
  count: currentError.count + 1
2261
2311
  });
2262
2312
  throw error;
2263
2313
  } finally {
2264
- this.loadingPromises.delete(routePath);
2314
+ this.loadingPromises.delete(cacheKey);
2265
2315
  }
2266
2316
  }
2267
2317
  async doLoadComponent(comp, routePath) {
@@ -2315,6 +2365,8 @@ var ComponentLoader = class {
2315
2365
  this.loadingPromises.clear();
2316
2366
  }
2317
2367
  };
2368
+ _ComponentLoader.MAX_ERROR_ENTRIES = 256;
2369
+ var ComponentLoader = _ComponentLoader;
2318
2370
  var _SibuRouter = class _SibuRouter {
2319
2371
  constructor(routes, options = {}) {
2320
2372
  // Event listeners cleanup
@@ -2380,7 +2432,7 @@ var _SibuRouter = class _SibuRouter {
2380
2432
  return window.location.hash.slice(1) || "/";
2381
2433
  }
2382
2434
  let path2 = window.location.pathname;
2383
- if (base2 && path2.startsWith(base2)) {
2435
+ if (base2 && (path2 === base2 || path2.startsWith(`${base2}/`))) {
2384
2436
  path2 = path2.slice(base2.length);
2385
2437
  }
2386
2438
  return (path2 || "/") + window.location.search + window.location.hash;
@@ -2513,7 +2565,7 @@ var _SibuRouter = class _SibuRouter {
2513
2565
  }
2514
2566
  if (to.params) {
2515
2567
  for (const [key, value] of Object.entries(to.params)) {
2516
- path2 = path2.replace(`:${key}`, encodeURIComponent(value));
2568
+ path2 = path2.replace(new RegExp(`:${key}(?=[/?#]|$)`, "g"), encodeURIComponent(value));
2517
2569
  }
2518
2570
  }
2519
2571
  if (to.query && Object.keys(to.query).length > 0) {
@@ -2749,8 +2801,7 @@ function Route() {
2749
2801
  let currentNode = null;
2750
2802
  let loadingNode = null;
2751
2803
  let errorNode = null;
2752
- let isUpdating = false;
2753
- let currentPath = "";
2804
+ let navSeq = 0;
2754
2805
  let currentTopRoute = null;
2755
2806
  const cleanupNodes = () => {
2756
2807
  [currentNode, loadingNode, errorNode].forEach((node) => {
@@ -2828,30 +2879,21 @@ function Route() {
2828
2879
  errorNode.appendChild(retryButton);
2829
2880
  anchor.parentNode.insertBefore(errorNode, anchor.nextSibling);
2830
2881
  };
2831
- let pendingUpdate = false;
2832
2882
  const update = async () => {
2833
2883
  if (!globalRouter) return;
2834
- if (isUpdating) {
2835
- pendingUpdate = true;
2836
- return;
2837
- }
2884
+ const seq = ++navSeq;
2838
2885
  const route2 = globalRouter.currentRoute;
2839
2886
  try {
2840
2887
  const match = globalRouter["matcher"].match(route2.path);
2841
2888
  if (!match) {
2842
- currentPath = route2.path;
2843
2889
  currentTopRoute = null;
2844
2890
  cleanupNodes();
2845
2891
  return;
2846
2892
  }
2847
2893
  const routeDef = match.matched[0] || match.route;
2848
2894
  if (routeDef === currentTopRoute && currentNode) {
2849
- currentPath = route2.path;
2850
2895
  return;
2851
2896
  }
2852
- isUpdating = true;
2853
- currentPath = route2.path;
2854
- currentTopRoute = routeDef;
2855
2897
  if ("redirect" in routeDef) {
2856
2898
  const redirectPath = typeof routeDef.redirect === "function" ? routeDef.redirect(route2) : routeDef.redirect;
2857
2899
  queueMicrotask(() => {
@@ -2868,27 +2910,25 @@ function Route() {
2868
2910
  showLoading();
2869
2911
  }
2870
2912
  const component = await globalRouter.loadComponent(routeDef, route2.path);
2913
+ if (seq !== navSeq) return;
2871
2914
  const node = component();
2872
- if (node && anchor.parentNode && route2.path === currentPath) {
2915
+ if (node && anchor.parentNode) {
2916
+ currentTopRoute = routeDef;
2873
2917
  cleanupNodes();
2874
2918
  anchor.parentNode.insertBefore(node, anchor.nextSibling);
2875
2919
  currentNode = node;
2876
2920
  }
2877
2921
  } catch (error) {
2922
+ if (seq !== navSeq) return;
2878
2923
  hideLoading();
2879
2924
  console.error("[Route] Component error:", error);
2880
2925
  showError(error instanceof Error ? error : new Error(String(error)), routeDef);
2881
2926
  }
2882
2927
  }
2883
2928
  } catch (error) {
2929
+ if (seq !== navSeq) return;
2884
2930
  console.error("[Route] Update failed:", error);
2885
2931
  showError(error instanceof Error ? error : new Error(String(error)));
2886
- } finally {
2887
- isUpdating = false;
2888
- if (pendingUpdate) {
2889
- pendingUpdate = false;
2890
- update();
2891
- }
2892
2932
  }
2893
2933
  };
2894
2934
  let routeInitialized = false;
@@ -2943,7 +2983,8 @@ function KeepAliveRoute(options) {
2943
2983
  return;
2944
2984
  }
2945
2985
  if (!("component" in routeDef)) return;
2946
- const cacheKey = route2.path;
2986
+ const queryStr = Object.keys(route2.query).length > 0 ? `?${new URLSearchParams(route2.query).toString()}` : "";
2987
+ const cacheKey = `${route2.path}${queryStr}${route2.hash ? `#${route2.hash}` : ""}`;
2947
2988
  const shouldCache = !includeNames || routeDef.name != null && includeNames.includes(routeDef.name);
2948
2989
  if (cacheKey === currentKey && currentNode) return;
2949
2990
  isUpdating = true;
@@ -3033,7 +3074,8 @@ function RouterLink(props) {
3033
3074
  const { to, replace: replace2 = false, activeClass, exactActiveClass, nodes, target, rel, class: classAttr, ...attrs } = props;
3034
3075
  const baseClass = typeof classAttr === "string" ? classAttr : "";
3035
3076
  const routeGetter = globalRouter.routeGetter;
3036
- const href = globalRouter["resolvePath"](to);
3077
+ const rawHref = globalRouter["resolvePath"](to);
3078
+ const href = isSafeNavigationTarget(rawHref) ? rawHref : "#";
3037
3079
  const hrefPath = href.split("?")[0].split("#")[0];
3038
3080
  const link2 = document.createElement("a");
3039
3081
  link2.href = href;
@@ -3065,9 +3107,18 @@ function RouterLink(props) {
3065
3107
  });
3066
3108
  registerDisposer(link2, effectCleanup);
3067
3109
  Object.entries(attrs).forEach(([key, value]) => {
3068
- if (key.startsWith("on") || key === "href") return;
3110
+ const lkey = key.toLowerCase();
3111
+ if (lkey === "href" || lkey[0] === "o" && lkey[1] === "n") return;
3069
3112
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
3070
- link2.setAttribute(key, String(value));
3113
+ const str = String(value);
3114
+ if (isUrlAttribute(lkey)) {
3115
+ const safe = sanitizeUrl(str);
3116
+ if (safe) link2.setAttribute(key, safe);
3117
+ } else if (lkey === "style") {
3118
+ link2.setAttribute(key, sanitizeCSSValue(str));
3119
+ } else {
3120
+ link2.setAttribute(key, str);
3121
+ }
3071
3122
  }
3072
3123
  });
3073
3124
  if (typeof nodes === "string") {
@@ -3224,24 +3275,43 @@ function __removeRouterPagehideHandler() {
3224
3275
  function Outlet() {
3225
3276
  const anchor = document.createComment("route-outlet-nested");
3226
3277
  let currentNode = null;
3278
+ let currentChild = null;
3279
+ let navSeq = 0;
3280
+ const clearCurrent = () => {
3281
+ if (currentNode) {
3282
+ dispose(currentNode);
3283
+ if (currentNode.parentNode) currentNode.parentNode.removeChild(currentNode);
3284
+ currentNode = null;
3285
+ }
3286
+ currentChild = null;
3287
+ };
3227
3288
  const update = async () => {
3228
3289
  if (!globalRouter) return;
3290
+ const seq = ++navSeq;
3229
3291
  const route2 = globalRouter.currentRoute;
3230
- if (route2.matched.length < 2) return;
3292
+ if (route2.matched.length < 2) {
3293
+ clearCurrent();
3294
+ return;
3295
+ }
3231
3296
  const childRoute = route2.matched[route2.matched.length - 1];
3232
- if (!childRoute || !("component" in childRoute)) return;
3297
+ if (!childRoute || !("component" in childRoute)) {
3298
+ clearCurrent();
3299
+ return;
3300
+ }
3301
+ if (childRoute === currentChild && currentNode) return;
3233
3302
  try {
3234
3303
  const cacheKey = `${route2.path}\0${childRoute.path}`;
3235
3304
  const component = await globalRouter.loadComponent(childRoute, cacheKey);
3305
+ if (seq !== navSeq) return;
3236
3306
  const node = component();
3237
3307
  if (node && anchor.parentNode) {
3238
- if (currentNode?.parentNode) {
3239
- currentNode.parentNode.removeChild(currentNode);
3240
- }
3308
+ clearCurrent();
3241
3309
  anchor.parentNode.insertBefore(node, anchor.nextSibling);
3242
3310
  currentNode = node;
3311
+ currentChild = childRoute;
3243
3312
  }
3244
3313
  } catch (error) {
3314
+ if (seq !== navSeq) return;
3245
3315
  console.error("[Outlet] Failed to render child route:", error);
3246
3316
  }
3247
3317
  };
@@ -3318,10 +3388,14 @@ function createMemoryRouter(routes, _initialPath = "/") {
3318
3388
 
3319
3389
  // src/plugins/routerSSR.ts
3320
3390
  init_ssr();
3321
- var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
3322
- function isForbiddenKey(key) {
3323
- return FORBIDDEN_KEYS.has(key);
3391
+
3392
+ // src/utils/guards.ts
3393
+ var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
3394
+ function isUnsafeKey(key) {
3395
+ return UNSAFE_KEYS.has(key);
3324
3396
  }
3397
+
3398
+ // src/plugins/routerSSR.ts
3325
3399
  function safeDecode(raw) {
3326
3400
  try {
3327
3401
  return decodeURIComponent(raw);
@@ -3362,7 +3436,7 @@ function parseURL(url) {
3362
3436
  key = safeDecode(pair.slice(0, eqIndex));
3363
3437
  value = safeDecode(pair.slice(eqIndex + 1));
3364
3438
  }
3365
- if (isForbiddenKey(key)) continue;
3439
+ if (isUnsafeKey(key)) continue;
3366
3440
  query[key] = value;
3367
3441
  }
3368
3442
  }
@@ -3424,7 +3498,7 @@ function matchRoute(path2, routes, parentPath = "", parentChain = []) {
3424
3498
  const params = nullObject();
3425
3499
  for (let i2 = 0; i2 < compiled.keys.length; i2++) {
3426
3500
  const key = compiled.keys[i2];
3427
- if (isForbiddenKey(key)) continue;
3501
+ if (isUnsafeKey(key)) continue;
3428
3502
  if (match[i2 + 1] !== void 0) {
3429
3503
  params[key] = safeDecode(match[i2 + 1]);
3430
3504
  }
@@ -3521,7 +3595,7 @@ function renderRouteToString(url, routes, _options) {
3521
3595
  function renderRouteToDocument(url, routes, options) {
3522
3596
  const { html: html2, state } = renderRouteToString(url, routes, options);
3523
3597
  const opts = options || {};
3524
- const metaTags = (opts.meta || []).map((attrs) => {
3598
+ const metaTags = (opts.meta || []).filter((attrs) => !isDangerousMetaRefresh(attrs)).map((attrs) => {
3525
3599
  const pairs = buildSafeAttrString(attrs);
3526
3600
  return pairs ? `<meta ${pairs} />` : "";
3527
3601
  }).filter(Boolean).join("\n ");
@@ -3601,7 +3675,7 @@ var SAFE_ATTR_NAME2 = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
3601
3675
  function isSafeAttrName2(name) {
3602
3676
  return SAFE_ATTR_NAME2.test(name);
3603
3677
  }
3604
- function isEventHandlerAttr3(name) {
3678
+ function isEventHandlerAttr2(name) {
3605
3679
  if (name.length < 3) return false;
3606
3680
  const lower = name.toLowerCase();
3607
3681
  return lower[0] === "o" && lower[1] === "n" && lower.charCodeAt(2) >= 97 && lower.charCodeAt(2) <= 122;
@@ -3634,7 +3708,7 @@ function buildSafeAttrString(attrs) {
3634
3708
  for (const rawKey of Object.keys(attrs)) {
3635
3709
  if (!Object.hasOwn(attrs, rawKey)) continue;
3636
3710
  if (!isSafeAttrName2(rawKey)) continue;
3637
- if (isEventHandlerAttr3(rawKey)) continue;
3711
+ if (isEventHandlerAttr2(rawKey)) continue;
3638
3712
  const lowerKey = rawKey.toLowerCase();
3639
3713
  let value = String(attrs[rawKey]);
3640
3714
  if (URL_ATTRS2.has(lowerKey)) {