sibujs 3.0.0 → 3.2.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 (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 +276 -150
  5. package/dist/build.js +35 -24
  6. package/dist/cdn.global.js +7 -7
  7. package/dist/{chunk-RJIRT46U.js → chunk-2C4E3HBM.js} +5 -5
  8. package/dist/{chunk-XDKP4T7G.js → chunk-4JCAUOLN.js} +45 -23
  9. package/dist/{chunk-VSNLICTS.js → chunk-5N74TKLD.js} +1 -1
  10. package/dist/{chunk-XVYB3J6C.js → chunk-7XDYVJLE.js} +19 -9
  11. package/dist/{chunk-L52H775O.js → chunk-BGNLPNGV.js} +20 -12
  12. package/dist/{chunk-6QZO7MMG.js → chunk-C427DVQF.js} +1 -1
  13. package/dist/{chunk-5WD7BYTZ.js → chunk-FDY42FIU.js} +3 -2
  14. package/dist/{chunk-4YTVESDX.js → chunk-FOI23UJL.js} +11 -1
  15. package/dist/{chunk-2RA7SHDA.js → chunk-GOJMFRBL.js} +20 -4
  16. package/dist/{chunk-2KM2724A.js → chunk-GOUM4JCT.js} +6 -6
  17. package/dist/chunk-H3SRKIYX.js +17 -0
  18. package/dist/{chunk-NEWH4O5U.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-DF3GTP4Q.js → chunk-JYXOEYI4.js} +12 -18
  22. package/dist/{chunk-KZA7ANXP.js → chunk-NFYWLRUO.js} +11 -18
  23. package/dist/{chunk-KH4OE6WY.js → chunk-NPIEEKPT.js} +20 -11
  24. package/dist/{chunk-V65KTDZW.js → chunk-OYLPZO4N.js} +33 -15
  25. package/dist/{chunk-LYTCUZ7H.js → chunk-RDRSWYNP.js} +1 -1
  26. package/dist/{chunk-UKMXT5T6.js → chunk-RLUJL2MV.js} +7 -12
  27. package/dist/{chunk-INBOWHQ3.js → chunk-V2MTG5FT.js} +99 -36
  28. package/dist/{chunk-CNZ35WI2.js → chunk-VJE6DDYM.js} +2 -2
  29. package/dist/{chunk-2JQUV4Y3.js → chunk-VOCE4NNK.js} +157 -75
  30. package/dist/{chunk-STFTTMO2.js → chunk-X67UYC74.js} +31 -12
  31. package/dist/{chunk-YMOIAHWA.js → chunk-YFDGQWDA.js} +1 -1
  32. package/dist/{chunk-L4DAT4WU.js → chunk-Z2FWAE4B.js} +28 -1
  33. package/dist/data.cjs +211 -93
  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 +163 -65
  42. package/dist/ecosystem.js +9 -9
  43. package/dist/extras.cjs +420 -198
  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 +255 -139
  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 +66 -39
  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 +243 -138
  62. package/dist/plugins.d.cts +1 -1
  63. package/dist/plugins.d.ts +1 -1
  64. package/dist/plugins.js +96 -45
  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 +185 -68
  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 +76 -39
  75. package/dist/ui.js +10 -9
  76. package/dist/widgets.cjs +61 -23
  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
  }
@@ -901,7 +917,7 @@ function retrack(effectFn, subscriber) {
901
917
  }
902
918
  }
903
919
  function track(effectFn, subscriber) {
904
- if (!subscriber) subscriber = effectFn;
920
+ if (!subscriber) return reactiveBinding(effectFn);
905
921
  cleanup(subscriber);
906
922
  const prev = currentSubscriber;
907
923
  currentSubscriber = subscriber;
@@ -919,6 +935,32 @@ function track(effectFn, subscriber) {
919
935
  const sub2 = subscriber;
920
936
  return sub2._dispose ?? (sub2._dispose = () => cleanup(subscriber));
921
937
  }
938
+ function reactiveBinding(commit) {
939
+ const run = () => {
940
+ const s2 = subscriber;
941
+ if (s2._disposed || s2._reentrant) return;
942
+ s2._reentrant = true;
943
+ try {
944
+ retrack(commit, subscriber);
945
+ } finally {
946
+ s2._reentrant = false;
947
+ }
948
+ };
949
+ const subscriber = run;
950
+ subscriber.depsHead = null;
951
+ subscriber.depsTail = null;
952
+ subscriber._epoch = 0;
953
+ subscriber._structDirty = false;
954
+ subscriber._runEpoch = 0;
955
+ subscriber._runs = 0;
956
+ subscriber._reentrant = false;
957
+ subscriber._disposed = false;
958
+ run();
959
+ return subscriber._dispose ?? (subscriber._dispose = () => {
960
+ subscriber._disposed = true;
961
+ cleanup(subscriber);
962
+ });
963
+ }
922
964
  function recordDependency(signal2) {
923
965
  if (!currentSubscriber) return;
924
966
  const sub2 = currentSubscriber;
@@ -1075,11 +1117,6 @@ var _isDev3 = isDev();
1075
1117
  function setProp(el, key, val) {
1076
1118
  el[key] = val;
1077
1119
  }
1078
- function isEventHandlerAttr(name) {
1079
- if (name.length < 3) return false;
1080
- const lower = name.toLowerCase();
1081
- return lower[0] === "o" && lower[1] === "n" && lower.charCodeAt(2) >= 97 && lower.charCodeAt(2) <= 122;
1082
- }
1083
1120
  function bindAttribute(el, attr, getter) {
1084
1121
  if (isEventHandlerAttr(attr)) {
1085
1122
  if (_isDev3)
@@ -1115,13 +1152,76 @@ function bindAttribute(el, attr, getter) {
1115
1152
  el.setAttribute(attr, isUrlAttribute(attr) ? sanitizeUrl(str) : str);
1116
1153
  }
1117
1154
  }
1118
- const teardown = track(commit);
1119
- return teardown;
1155
+ return reactiveBinding(commit);
1120
1156
  }
1121
1157
 
1122
1158
  // src/reactivity/bindChildNode.ts
1123
1159
  init_dev();
1160
+
1161
+ // src/core/rendering/dispose.ts
1162
+ init_dev();
1163
+ var elementDisposers = /* @__PURE__ */ new WeakMap();
1124
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();
1125
1225
  function bindChildNode(placeholder, getter) {
1126
1226
  let lastNodes = [];
1127
1227
  function commit() {
@@ -1129,12 +1229,13 @@ function bindChildNode(placeholder, getter) {
1129
1229
  try {
1130
1230
  result = getter();
1131
1231
  } catch (err) {
1132
- 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)}`);
1133
1233
  return;
1134
1234
  }
1135
1235
  if (result == null || typeof result === "boolean") {
1136
1236
  for (let i2 = 0; i2 < lastNodes.length; i2++) {
1137
1237
  const node = lastNodes[i2];
1238
+ dispose(node);
1138
1239
  if (node.parentNode) node.parentNode.removeChild(node);
1139
1240
  }
1140
1241
  lastNodes.length = 0;
@@ -1154,7 +1255,7 @@ function bindChildNode(placeholder, getter) {
1154
1255
  if (item == null || typeof item === "boolean") continue;
1155
1256
  const node = item instanceof Node ? item : document.createTextNode(String(item));
1156
1257
  if (seen.has(node)) {
1157
- if (_isDev4)
1258
+ if (_isDev5)
1158
1259
  devWarn("bindChildNode: duplicate node reference in array \u2014 only the first occurrence is rendered.");
1159
1260
  continue;
1160
1261
  }
@@ -1176,90 +1277,24 @@ function bindChildNode(placeholder, getter) {
1176
1277
  for (let i2 = 0; i2 < lastNodes.length; i2++) {
1177
1278
  const node = lastNodes[i2];
1178
1279
  if (reused?.has(node)) continue;
1280
+ dispose(node);
1179
1281
  if (node.parentNode) node.parentNode.removeChild(node);
1180
1282
  }
1181
- const anchor = placeholder.nextSibling;
1283
+ let prev = placeholder;
1182
1284
  for (let i2 = 0; i2 < newNodes.length; i2++) {
1183
1285
  const node = newNodes[i2];
1184
- if (reused?.has(node) && node.parentNode === parent) {
1185
- if (node.nextSibling !== anchor) {
1186
- parent.insertBefore(node, anchor);
1187
- }
1188
- } else {
1189
- parent.insertBefore(node, anchor);
1286
+ if (prev.nextSibling !== node) {
1287
+ parent.insertBefore(node, prev.nextSibling);
1190
1288
  }
1289
+ prev = node;
1191
1290
  }
1192
1291
  lastNodes = newNodes;
1193
1292
  }
1194
- return track(commit);
1293
+ return reactiveBinding(commit);
1195
1294
  }
1196
1295
 
1197
1296
  // src/core/rendering/tagFactory.ts
1198
1297
  init_sanitize();
1199
-
1200
- // src/core/rendering/dispose.ts
1201
- init_dev();
1202
- var elementDisposers = /* @__PURE__ */ new WeakMap();
1203
- var _isDev5 = isDev();
1204
- var activeBindingCount = 0;
1205
- function registerDisposer(node, teardown) {
1206
- let disposers = elementDisposers.get(node);
1207
- if (!disposers) {
1208
- disposers = [];
1209
- elementDisposers.set(node, disposers);
1210
- }
1211
- disposers.push(teardown);
1212
- if (_isDev5) activeBindingCount++;
1213
- }
1214
- function dispose(node) {
1215
- const stack = [node];
1216
- const order = [];
1217
- while (stack.length > 0) {
1218
- const current = stack.pop();
1219
- order.push(current);
1220
- const children = Array.from(current.childNodes);
1221
- for (let i2 = 0; i2 < children.length; i2++) {
1222
- stack.push(children[i2]);
1223
- }
1224
- }
1225
- for (let i2 = order.length - 1; i2 >= 0; i2--) {
1226
- const current = order[i2];
1227
- const disposers = elementDisposers.get(current);
1228
- if (disposers) {
1229
- const snapshot = disposers.slice();
1230
- elementDisposers.delete(current);
1231
- if (_isDev5) activeBindingCount -= snapshot.length;
1232
- for (const d of snapshot) {
1233
- try {
1234
- d();
1235
- } catch (err) {
1236
- if (_isDev5 && typeof console !== "undefined") {
1237
- console.warn("[SibuJS] Disposer threw during cleanup:", err);
1238
- }
1239
- }
1240
- }
1241
- let extraPasses = 0;
1242
- while (extraPasses++ < 8) {
1243
- const added = elementDisposers.get(current);
1244
- if (!added || added.length === 0) break;
1245
- const moreSnapshot = added.slice();
1246
- elementDisposers.delete(current);
1247
- if (_isDev5) activeBindingCount -= moreSnapshot.length;
1248
- for (const d of moreSnapshot) {
1249
- try {
1250
- d();
1251
- } catch (err) {
1252
- if (_isDev5 && typeof console !== "undefined") {
1253
- console.warn("[SibuJS] Disposer threw during cleanup:", err);
1254
- }
1255
- }
1256
- }
1257
- }
1258
- }
1259
- }
1260
- }
1261
-
1262
- // src/core/rendering/tagFactory.ts
1263
1298
  var SVG_NS = "http://www.w3.org/2000/svg";
1264
1299
  var _isDev6 = isDev();
1265
1300
  var BLOCKED_TAGS = /* @__PURE__ */ new Set(["script", "iframe", "object", "embed", "frame", "frameset"]);
@@ -1285,6 +1320,18 @@ var CLOBBER_RISKY_IDS = /* @__PURE__ */ new Set([
1285
1320
  function setProp2(el, key, val) {
1286
1321
  el[key] = val;
1287
1322
  }
1323
+ function looksLikeClassList(s2) {
1324
+ const t2 = s2.trim();
1325
+ if (!t2) return false;
1326
+ const tokens = t2.split(/\s+/);
1327
+ let sawClassish = false;
1328
+ for (let i2 = 0; i2 < tokens.length; i2++) {
1329
+ const tok = tokens[i2];
1330
+ if (!/^-?[A-Za-z_][A-Za-z0-9_:/.-]*$/.test(tok)) return false;
1331
+ if (/[-:/0-9]/.test(tok)) sawClassish = true;
1332
+ }
1333
+ return sawClassish;
1334
+ }
1288
1335
  var kebabCache = /* @__PURE__ */ new Map();
1289
1336
  function toKebab(prop) {
1290
1337
  let cached = kebabCache.get(prop);
@@ -1420,6 +1467,11 @@ var tagFactory = (tag, ns) => {
1420
1467
  appendChildren(el, second);
1421
1468
  return el;
1422
1469
  }
1470
+ if (_isDev6 && looksLikeClassList(first)) {
1471
+ devWarn(
1472
+ `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.`
1473
+ );
1474
+ }
1423
1475
  el.textContent = first;
1424
1476
  return el;
1425
1477
  }
@@ -1477,7 +1529,7 @@ var tagFactory = (tag, ns) => {
1477
1529
  const value = props[key];
1478
1530
  if (value == null) continue;
1479
1531
  const lkey = key.toLowerCase();
1480
- if (lkey[0] === "o" && lkey[1] === "n") continue;
1532
+ if (isEventHandlerAttr(key)) continue;
1481
1533
  if (typeof value === "function") {
1482
1534
  registerDisposer(el, bindAttribute(el, key, value));
1483
1535
  } else if (typeof value === "boolean") {
@@ -1841,6 +1893,7 @@ function effect(effectFn, options) {
1841
1893
  ctx.fn(ctx.onCleanup);
1842
1894
  };
1843
1895
  const sub2 = (() => {
1896
+ if (ctx.disposed) return;
1844
1897
  if (ctx.running) {
1845
1898
  ctx.rerunPending = true;
1846
1899
  return;
@@ -1880,7 +1933,9 @@ function effect(effectFn, options) {
1880
1933
  init_sanitize();
1881
1934
  function isSafeNavigationTarget(path2) {
1882
1935
  if (path2 === "") return true;
1883
- return sanitizeUrl(path2) !== "";
1936
+ const normalized = stripControlChars(path2).replace(/\\/g, "/");
1937
+ if (normalized.startsWith("//")) return false;
1938
+ return sanitizeUrl(normalized) !== "";
1884
1939
  }
1885
1940
  var LRUCache = class {
1886
1941
  constructor(maxSize = 100) {
@@ -2017,8 +2072,13 @@ var RouteMatcher = class {
2017
2072
  if (match) {
2018
2073
  const params = {};
2019
2074
  compiled.keys.forEach((key, i2) => {
2020
- if (match[i2 + 1] !== void 0) {
2021
- 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
+ }
2022
2082
  }
2023
2083
  });
2024
2084
  return { params };
@@ -2069,14 +2129,23 @@ var RouteMatcher = class {
2069
2129
  }
2070
2130
  }
2071
2131
  removeRoute(path2) {
2072
- this.routeTrie.delete(path2);
2073
- this.parentChain.delete(path2);
2074
2132
  this.compiledPatterns.clear();
2075
- for (const [name, route2] of this.namedRoutes) {
2076
- if (route2.path === path2) {
2077
- this.namedRoutes.delete(name);
2078
- break;
2079
- }
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);
2080
2149
  }
2081
2150
  }
2082
2151
  };
@@ -2188,42 +2257,61 @@ var GuardManager = class {
2188
2257
  this.afterEachHooks = [];
2189
2258
  }
2190
2259
  };
2191
- var ComponentLoader = class {
2260
+ var _ComponentLoader = class _ComponentLoader {
2192
2261
  constructor(cacheSize = 50, retryDelay = 1e3) {
2193
2262
  this.errorCache = /* @__PURE__ */ new Map();
2194
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;
2195
2270
  this.componentCache = new LRUCache(cacheSize);
2196
2271
  this.retryDelay = retryDelay;
2197
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
+ }
2198
2281
  async loadComponent(route2, routePath) {
2199
2282
  if (!("component" in route2)) {
2200
2283
  throw new Error(`Route ${routePath} does not have a component`);
2201
2284
  }
2202
2285
  const comp = route2.component;
2203
- const cached = this.componentCache.get(routePath);
2286
+ const cacheKey = this.keyFor(route2);
2287
+ const cached = this.componentCache.get(cacheKey);
2204
2288
  if (cached) return cached;
2205
- const existingPromise = this.loadingPromises.get(routePath);
2289
+ const existingPromise = this.loadingPromises.get(cacheKey);
2206
2290
  if (existingPromise) return existingPromise;
2207
- const errorInfo = this.errorCache.get(routePath);
2291
+ const errorInfo = this.errorCache.get(cacheKey);
2208
2292
  if (errorInfo && Date.now() - errorInfo.timestamp < this.retryDelay) {
2209
2293
  throw new Error(`Component loading failed recently, retry in ${this.retryDelay}ms`);
2210
2294
  }
2211
2295
  const loadingPromise = this.doLoadComponent(comp, routePath);
2212
- this.loadingPromises.set(routePath, loadingPromise);
2296
+ this.loadingPromises.set(cacheKey, loadingPromise);
2213
2297
  try {
2214
2298
  const component = await loadingPromise;
2215
- this.componentCache.set(routePath, component);
2216
- this.errorCache.delete(routePath);
2299
+ this.componentCache.set(cacheKey, component);
2300
+ this.errorCache.delete(cacheKey);
2217
2301
  return component;
2218
2302
  } catch (error) {
2219
- const currentError = this.errorCache.get(routePath) || { timestamp: 0, count: 0 };
2220
- 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, {
2221
2309
  timestamp: Date.now(),
2222
2310
  count: currentError.count + 1
2223
2311
  });
2224
2312
  throw error;
2225
2313
  } finally {
2226
- this.loadingPromises.delete(routePath);
2314
+ this.loadingPromises.delete(cacheKey);
2227
2315
  }
2228
2316
  }
2229
2317
  async doLoadComponent(comp, routePath) {
@@ -2277,6 +2365,8 @@ var ComponentLoader = class {
2277
2365
  this.loadingPromises.clear();
2278
2366
  }
2279
2367
  };
2368
+ _ComponentLoader.MAX_ERROR_ENTRIES = 256;
2369
+ var ComponentLoader = _ComponentLoader;
2280
2370
  var _SibuRouter = class _SibuRouter {
2281
2371
  constructor(routes, options = {}) {
2282
2372
  // Event listeners cleanup
@@ -2342,7 +2432,7 @@ var _SibuRouter = class _SibuRouter {
2342
2432
  return window.location.hash.slice(1) || "/";
2343
2433
  }
2344
2434
  let path2 = window.location.pathname;
2345
- if (base2 && path2.startsWith(base2)) {
2435
+ if (base2 && (path2 === base2 || path2.startsWith(`${base2}/`))) {
2346
2436
  path2 = path2.slice(base2.length);
2347
2437
  }
2348
2438
  return (path2 || "/") + window.location.search + window.location.hash;
@@ -2475,7 +2565,7 @@ var _SibuRouter = class _SibuRouter {
2475
2565
  }
2476
2566
  if (to.params) {
2477
2567
  for (const [key, value] of Object.entries(to.params)) {
2478
- path2 = path2.replace(`:${key}`, encodeURIComponent(value));
2568
+ path2 = path2.replace(new RegExp(`:${key}(?=[/?#]|$)`, "g"), encodeURIComponent(value));
2479
2569
  }
2480
2570
  }
2481
2571
  if (to.query && Object.keys(to.query).length > 0) {
@@ -2905,7 +2995,8 @@ function KeepAliveRoute(options) {
2905
2995
  return;
2906
2996
  }
2907
2997
  if (!("component" in routeDef)) return;
2908
- const cacheKey = route2.path;
2998
+ const queryStr = Object.keys(route2.query).length > 0 ? `?${new URLSearchParams(route2.query).toString()}` : "";
2999
+ const cacheKey = `${route2.path}${queryStr}${route2.hash ? `#${route2.hash}` : ""}`;
2909
3000
  const shouldCache = !includeNames || routeDef.name != null && includeNames.includes(routeDef.name);
2910
3001
  if (cacheKey === currentKey && currentNode) return;
2911
3002
  isUpdating = true;
@@ -2995,7 +3086,8 @@ function RouterLink(props) {
2995
3086
  const { to, replace: replace2 = false, activeClass, exactActiveClass, nodes, target, rel, class: classAttr, ...attrs } = props;
2996
3087
  const baseClass = typeof classAttr === "string" ? classAttr : "";
2997
3088
  const routeGetter = globalRouter.routeGetter;
2998
- const href = globalRouter["resolvePath"](to);
3089
+ const rawHref = globalRouter["resolvePath"](to);
3090
+ const href = isSafeNavigationTarget(rawHref) ? rawHref : "#";
2999
3091
  const hrefPath = href.split("?")[0].split("#")[0];
3000
3092
  const link2 = document.createElement("a");
3001
3093
  link2.href = href;
@@ -3027,9 +3119,18 @@ function RouterLink(props) {
3027
3119
  });
3028
3120
  registerDisposer(link2, effectCleanup);
3029
3121
  Object.entries(attrs).forEach(([key, value]) => {
3030
- if (key.startsWith("on") || key === "href") return;
3122
+ const lkey = key.toLowerCase();
3123
+ if (lkey === "href" || lkey[0] === "o" && lkey[1] === "n") return;
3031
3124
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
3032
- link2.setAttribute(key, String(value));
3125
+ const str = String(value);
3126
+ if (isUrlAttribute(lkey)) {
3127
+ const safe = sanitizeUrl(str);
3128
+ if (safe) link2.setAttribute(key, safe);
3129
+ } else if (lkey === "style") {
3130
+ link2.setAttribute(key, sanitizeCSSValue(str));
3131
+ } else {
3132
+ link2.setAttribute(key, str);
3133
+ }
3033
3134
  }
3034
3135
  });
3035
3136
  if (typeof nodes === "string") {
@@ -3280,10 +3381,14 @@ function createMemoryRouter(routes, _initialPath = "/") {
3280
3381
 
3281
3382
  // src/plugins/routerSSR.ts
3282
3383
  init_ssr();
3283
- var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
3284
- function isForbiddenKey(key) {
3285
- return FORBIDDEN_KEYS.has(key);
3384
+
3385
+ // src/utils/guards.ts
3386
+ var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
3387
+ function isUnsafeKey(key) {
3388
+ return UNSAFE_KEYS.has(key);
3286
3389
  }
3390
+
3391
+ // src/plugins/routerSSR.ts
3287
3392
  function safeDecode(raw) {
3288
3393
  try {
3289
3394
  return decodeURIComponent(raw);
@@ -3324,7 +3429,7 @@ function parseURL(url) {
3324
3429
  key = safeDecode(pair.slice(0, eqIndex));
3325
3430
  value = safeDecode(pair.slice(eqIndex + 1));
3326
3431
  }
3327
- if (isForbiddenKey(key)) continue;
3432
+ if (isUnsafeKey(key)) continue;
3328
3433
  query[key] = value;
3329
3434
  }
3330
3435
  }
@@ -3386,7 +3491,7 @@ function matchRoute(path2, routes, parentPath = "", parentChain = []) {
3386
3491
  const params = nullObject();
3387
3492
  for (let i2 = 0; i2 < compiled.keys.length; i2++) {
3388
3493
  const key = compiled.keys[i2];
3389
- if (isForbiddenKey(key)) continue;
3494
+ if (isUnsafeKey(key)) continue;
3390
3495
  if (match[i2 + 1] !== void 0) {
3391
3496
  params[key] = safeDecode(match[i2 + 1]);
3392
3497
  }
@@ -3483,7 +3588,7 @@ function renderRouteToString(url, routes, _options) {
3483
3588
  function renderRouteToDocument(url, routes, options) {
3484
3589
  const { html: html2, state } = renderRouteToString(url, routes, options);
3485
3590
  const opts = options || {};
3486
- const metaTags = (opts.meta || []).map((attrs) => {
3591
+ const metaTags = (opts.meta || []).filter((attrs) => !isDangerousMetaRefresh(attrs)).map((attrs) => {
3487
3592
  const pairs = buildSafeAttrString(attrs);
3488
3593
  return pairs ? `<meta ${pairs} />` : "";
3489
3594
  }).filter(Boolean).join("\n ");
@@ -3563,7 +3668,7 @@ var SAFE_ATTR_NAME2 = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
3563
3668
  function isSafeAttrName2(name) {
3564
3669
  return SAFE_ATTR_NAME2.test(name);
3565
3670
  }
3566
- function isEventHandlerAttr3(name) {
3671
+ function isEventHandlerAttr2(name) {
3567
3672
  if (name.length < 3) return false;
3568
3673
  const lower = name.toLowerCase();
3569
3674
  return lower[0] === "o" && lower[1] === "n" && lower.charCodeAt(2) >= 97 && lower.charCodeAt(2) <= 122;
@@ -3596,7 +3701,7 @@ function buildSafeAttrString(attrs) {
3596
3701
  for (const rawKey of Object.keys(attrs)) {
3597
3702
  if (!Object.hasOwn(attrs, rawKey)) continue;
3598
3703
  if (!isSafeAttrName2(rawKey)) continue;
3599
- if (isEventHandlerAttr3(rawKey)) continue;
3704
+ if (isEventHandlerAttr2(rawKey)) continue;
3600
3705
  const lowerKey = rawKey.toLowerCase();
3601
3706
  let value = String(attrs[rawKey]);
3602
3707
  if (URL_ATTRS2.has(lowerKey)) {