@wsxjs/wsx-core 0.0.19 → 0.0.21

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.
package/dist/index.js CHANGED
@@ -37,6 +37,170 @@ __export(index_exports, {
37
37
  });
38
38
  module.exports = __toCommonJS(index_exports);
39
39
 
40
+ // src/utils/dom-utils.ts
41
+ function parseHTMLToNodes(html) {
42
+ if (!html) return [];
43
+ const temp = document.createElement("div");
44
+ temp.innerHTML = html;
45
+ return Array.from(temp.childNodes).map((node) => {
46
+ if (node instanceof HTMLElement || node instanceof SVGElement) {
47
+ return node;
48
+ } else {
49
+ return node.textContent || "";
50
+ }
51
+ });
52
+ }
53
+ function isHTMLString(str) {
54
+ const trimmed = str.trim();
55
+ if (!trimmed) return false;
56
+ const htmlTagPattern = /<[a-z][a-z0-9]*(\s[^>]*)?(\/>|>)/i;
57
+ const looksLikeMath = /^[^<]*<[^>]*>[^>]*$/.test(trimmed) && !htmlTagPattern.test(trimmed);
58
+ if (looksLikeMath) return false;
59
+ return htmlTagPattern.test(trimmed);
60
+ }
61
+ function flattenChildren(children, skipHTMLDetection = false, depth = 0) {
62
+ if (depth > 10) {
63
+ console.warn(
64
+ "[WSX] flattenChildren: Maximum depth exceeded, treating remaining children as text"
65
+ );
66
+ return children.filter(
67
+ (child) => typeof child === "string" || typeof child === "number"
68
+ );
69
+ }
70
+ const result = [];
71
+ for (const child of children) {
72
+ if (child === null || child === void 0 || child === false) {
73
+ continue;
74
+ } else if (Array.isArray(child)) {
75
+ result.push(...flattenChildren(child, skipHTMLDetection, depth + 1));
76
+ } else if (typeof child === "string") {
77
+ if (skipHTMLDetection) {
78
+ result.push(child);
79
+ } else if (isHTMLString(child)) {
80
+ try {
81
+ const nodes = parseHTMLToNodes(child);
82
+ if (nodes.length > 0) {
83
+ for (const node of nodes) {
84
+ if (typeof node === "string") {
85
+ result.push(node);
86
+ } else {
87
+ result.push(node);
88
+ }
89
+ }
90
+ } else {
91
+ result.push(child);
92
+ }
93
+ } catch (error) {
94
+ console.warn("[WSX] Failed to parse HTML string, treating as text:", error);
95
+ result.push(child);
96
+ }
97
+ } else {
98
+ result.push(child);
99
+ }
100
+ } else {
101
+ result.push(child);
102
+ }
103
+ }
104
+ return result;
105
+ }
106
+
107
+ // src/render-context.ts
108
+ var _RenderContext = class _RenderContext {
109
+ /**
110
+ * Executes a function within the context of a component.
111
+ * @param component The component instance currently rendering.
112
+ * @param fn The function to execute (usually the render method).
113
+ */
114
+ static runInContext(component, fn) {
115
+ const prev = _RenderContext.current;
116
+ _RenderContext.current = component;
117
+ try {
118
+ return fn();
119
+ } finally {
120
+ _RenderContext.current = prev;
121
+ }
122
+ }
123
+ /**
124
+ * Gets the currently rendering component.
125
+ */
126
+ static getCurrentComponent() {
127
+ return _RenderContext.current;
128
+ }
129
+ /**
130
+ * Gets the current component's DOM cache.
131
+ */
132
+ static getDOMCache() {
133
+ return _RenderContext.current?.getDomCache();
134
+ }
135
+ };
136
+ _RenderContext.current = null;
137
+ var RenderContext = _RenderContext;
138
+
139
+ // src/utils/cache-key.ts
140
+ var POSITION_ID_KEY = "__wsxPositionId";
141
+ var INDEX_KEY = "__wsxIndex";
142
+ var componentElementCounters = /* @__PURE__ */ new WeakMap();
143
+ var componentIdCache = /* @__PURE__ */ new WeakMap();
144
+ function generateCacheKey(tag, props, componentId, component) {
145
+ const positionId = props?.[POSITION_ID_KEY];
146
+ const userKey = props?.key;
147
+ const index = props?.[INDEX_KEY];
148
+ if (userKey !== void 0 && userKey !== null) {
149
+ return `${componentId}:${tag}:key-${String(userKey)}`;
150
+ }
151
+ if (index !== void 0 && index !== null) {
152
+ return `${componentId}:${tag}:idx-${String(index)}`;
153
+ }
154
+ if (positionId !== void 0 && positionId !== null && positionId !== "no-id") {
155
+ return `${componentId}:${tag}:${String(positionId)}`;
156
+ }
157
+ if (component) {
158
+ let counter = componentElementCounters.get(component) || 0;
159
+ counter++;
160
+ componentElementCounters.set(component, counter);
161
+ return `${componentId}:${tag}:auto-${counter}`;
162
+ }
163
+ return `${componentId}:${tag}:fallback-${Date.now()}-${Math.random()}`;
164
+ }
165
+ function getComponentId() {
166
+ const component = RenderContext.getCurrentComponent();
167
+ if (component) {
168
+ let cachedId = componentIdCache.get(component);
169
+ if (cachedId) {
170
+ return cachedId;
171
+ }
172
+ const instanceId = component._instanceId || "default";
173
+ cachedId = `${component.constructor.name}:${instanceId}`;
174
+ componentIdCache.set(component, cachedId);
175
+ return cachedId;
176
+ }
177
+ return "unknown";
178
+ }
179
+
180
+ // src/utils/element-marking.ts
181
+ var CACHE_KEY_PROP = "__wsxCacheKey";
182
+ function markElement(element, cacheKey) {
183
+ element[CACHE_KEY_PROP] = cacheKey;
184
+ }
185
+ function isCreatedByH(element) {
186
+ if (!(element instanceof HTMLElement || element instanceof SVGElement)) {
187
+ return false;
188
+ }
189
+ return element[CACHE_KEY_PROP] !== void 0;
190
+ }
191
+ function shouldPreserveElement(element) {
192
+ if (!(element instanceof HTMLElement || element instanceof SVGElement)) {
193
+ return true;
194
+ }
195
+ if (!isCreatedByH(element)) {
196
+ return true;
197
+ }
198
+ if (element.hasAttribute("data-wsx-preserve")) {
199
+ return true;
200
+ }
201
+ return false;
202
+ }
203
+
40
204
  // src/utils/svg-utils.ts
41
205
  var SVG_NAMESPACE = "http://www.w3.org/2000/svg";
42
206
  var SVG_ONLY_ELEMENTS = /* @__PURE__ */ new Set([
@@ -149,62 +313,301 @@ function getSVGAttributeName(attributeName) {
149
313
  return SVG_ATTRIBUTE_MAP.get(attributeName) || attributeName;
150
314
  }
151
315
 
152
- // src/utils/dom-utils.ts
153
- function parseHTMLToNodes(html) {
154
- if (!html) return [];
155
- const temp = document.createElement("div");
156
- temp.innerHTML = html;
157
- return Array.from(temp.childNodes).map((node) => {
158
- if (node instanceof HTMLElement || node instanceof SVGElement) {
159
- return node;
160
- } else {
161
- return node.textContent || "";
316
+ // src/utils/logger.ts
317
+ var WSXLogger = class {
318
+ constructor(prefix = "[WSX]", enabled = true, level = "info") {
319
+ this.prefix = prefix;
320
+ this.enabled = enabled;
321
+ this.level = level;
322
+ }
323
+ shouldLog(level) {
324
+ if (!this.enabled) return false;
325
+ const levels = ["debug", "info", "warn", "error"];
326
+ const currentLevelIndex = levels.indexOf(this.level);
327
+ const messageLevelIndex = levels.indexOf(level);
328
+ return messageLevelIndex >= currentLevelIndex;
329
+ }
330
+ debug(message, ...args) {
331
+ if (this.shouldLog("debug")) {
332
+ console.debug(`${this.prefix} ${message}`, ...args);
162
333
  }
163
- });
334
+ }
335
+ info(message, ...args) {
336
+ if (this.shouldLog("info")) {
337
+ console.info(`${this.prefix} ${message}`, ...args);
338
+ }
339
+ }
340
+ warn(message, ...args) {
341
+ if (this.shouldLog("warn")) {
342
+ console.warn(`${this.prefix} ${message}`, ...args);
343
+ }
344
+ }
345
+ error(message, ...args) {
346
+ if (this.shouldLog("error")) {
347
+ console.error(`${this.prefix} ${message}`, ...args);
348
+ }
349
+ }
350
+ };
351
+ var logger = new WSXLogger();
352
+ function createLogger(componentName) {
353
+ return new WSXLogger(`[WSX:${componentName}]`);
164
354
  }
165
355
 
166
- // src/jsx-factory.ts
167
- function h(tag, props = {}, ...children) {
168
- if (typeof tag === "function") {
169
- return tag(props, children);
356
+ // src/utils/props-utils.ts
357
+ var logger2 = createLogger("Props Utilities");
358
+ function isFrameworkInternalProp(key) {
359
+ if (key === "key") {
360
+ return true;
170
361
  }
171
- const element = createElement(tag);
172
- if (props) {
173
- const isSVG = shouldUseSVGNamespace(tag);
174
- Object.entries(props).forEach(([key, value]) => {
175
- if (value === null || value === void 0 || value === false) {
176
- return;
177
- }
178
- if (key === "ref" && typeof value === "function") {
179
- value(element);
180
- } else if (key === "className" || key === "class") {
181
- if (isSVG) {
182
- element.setAttribute("class", value);
183
- } else {
184
- element.className = value;
362
+ if (key === "__wsxPositionId" || key === "__wsxIndex") {
363
+ return true;
364
+ }
365
+ if (key === "__testId") {
366
+ return true;
367
+ }
368
+ if (key === "ref") {
369
+ return true;
370
+ }
371
+ return false;
372
+ }
373
+ function isStandardHTMLAttribute(key) {
374
+ const standardAttributes = /* @__PURE__ */ new Set([
375
+ // 全局属性
376
+ "id",
377
+ "class",
378
+ "className",
379
+ "style",
380
+ "title",
381
+ "lang",
382
+ "dir",
383
+ "hidden",
384
+ "tabindex",
385
+ "accesskey",
386
+ "contenteditable",
387
+ "draggable",
388
+ "spellcheck",
389
+ "translate",
390
+ "autocapitalize",
391
+ "autocorrect",
392
+ // 表单属性
393
+ "name",
394
+ "value",
395
+ "type",
396
+ "placeholder",
397
+ "required",
398
+ "disabled",
399
+ "readonly",
400
+ "checked",
401
+ "selected",
402
+ "multiple",
403
+ "min",
404
+ "max",
405
+ "step",
406
+ "autocomplete",
407
+ "autofocus",
408
+ "form",
409
+ "formaction",
410
+ "formenctype",
411
+ "formmethod",
412
+ "formnovalidate",
413
+ "formtarget",
414
+ // 链接属性
415
+ "href",
416
+ "target",
417
+ "rel",
418
+ "download",
419
+ "hreflang",
420
+ "ping",
421
+ // 媒体属性
422
+ "src",
423
+ "alt",
424
+ "width",
425
+ "height",
426
+ "poster",
427
+ "preload",
428
+ "controls",
429
+ "autoplay",
430
+ "loop",
431
+ "muted",
432
+ "playsinline",
433
+ "crossorigin",
434
+ // ARIA 属性(部分常见)
435
+ "role"
436
+ ]);
437
+ const lowerKey = key.toLowerCase();
438
+ if (standardAttributes.has(lowerKey)) {
439
+ return true;
440
+ }
441
+ if (lowerKey.startsWith("data-")) {
442
+ return true;
443
+ }
444
+ if (lowerKey.startsWith("aria-")) {
445
+ return true;
446
+ }
447
+ if (key.startsWith("xml:") || key.startsWith("xlink:")) {
448
+ return true;
449
+ }
450
+ return false;
451
+ }
452
+ function isSpecialProperty(key, value) {
453
+ return key === "ref" || key === "className" || key === "class" || key === "style" || key.startsWith("on") && typeof value === "function" || typeof value === "boolean" || key === "value";
454
+ }
455
+ function setSmartProperty(element, key, value, tag) {
456
+ const isSVG = shouldUseSVGNamespace(tag);
457
+ if (isSpecialProperty(key, value)) {
458
+ return;
459
+ }
460
+ if (isStandardHTMLAttribute(key)) {
461
+ const attributeName = isSVG ? getSVGAttributeName(key) : key;
462
+ if (typeof value === "object" && value !== null) {
463
+ try {
464
+ const serialized = JSON.stringify(value);
465
+ if (serialized.length > 1024 * 1024) {
466
+ logger2.warn(
467
+ `[WSX] Attribute "${key}" value too large, consider using a non-standard property name instead`
468
+ );
185
469
  }
186
- } else if (key === "style" && typeof value === "string") {
187
- element.setAttribute("style", value);
188
- } else if (key.startsWith("on") && typeof value === "function") {
189
- const eventName = key.slice(2).toLowerCase();
190
- element.addEventListener(eventName, value);
191
- } else if (typeof value === "boolean") {
192
- if (value) {
193
- element.setAttribute(key, "");
470
+ element.setAttribute(attributeName, serialized);
471
+ } catch (error) {
472
+ logger2.warn(`Cannot serialize attribute "${key}":`, error);
473
+ }
474
+ } else {
475
+ element.setAttribute(attributeName, String(value));
476
+ }
477
+ return;
478
+ }
479
+ if (element instanceof SVGElement) {
480
+ const attributeName = getSVGAttributeName(key);
481
+ if (typeof value === "object" && value !== null) {
482
+ try {
483
+ const serialized = JSON.stringify(value);
484
+ element.setAttribute(attributeName, serialized);
485
+ } catch (error) {
486
+ logger2.warn(`Cannot serialize SVG attribute "${key}":`, error);
487
+ }
488
+ } else {
489
+ element.setAttribute(attributeName, String(value));
490
+ }
491
+ return;
492
+ }
493
+ const hasProperty = key in element || Object.prototype.hasOwnProperty.call(element, key);
494
+ if (hasProperty) {
495
+ let isReadOnly = false;
496
+ try {
497
+ const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(element), key);
498
+ if (descriptor) {
499
+ isReadOnly = descriptor.get !== void 0 && descriptor.set === void 0 || descriptor.writable === false && descriptor.set === void 0;
500
+ }
501
+ } catch {
502
+ }
503
+ if (isReadOnly) {
504
+ const attributeName = isSVG ? getSVGAttributeName(key) : key;
505
+ if (typeof value === "object" && value !== null) {
506
+ try {
507
+ const serialized = JSON.stringify(value);
508
+ element.setAttribute(attributeName, serialized);
509
+ } catch (error) {
510
+ logger2.warn(`Cannot serialize readonly property "${key}":`, error);
194
511
  }
195
- } else if (key === "value") {
196
- if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) {
197
- element.value = String(value);
512
+ } else {
513
+ element.setAttribute(attributeName, String(value));
514
+ }
515
+ } else {
516
+ try {
517
+ element[key] = value;
518
+ } catch {
519
+ const attributeName = isSVG ? getSVGAttributeName(key) : key;
520
+ if (typeof value === "object" && value !== null) {
521
+ try {
522
+ const serialized = JSON.stringify(value);
523
+ element.setAttribute(attributeName, serialized);
524
+ } catch (error) {
525
+ logger2.warn(
526
+ `[WSX] Cannot serialize property "${key}" for attribute:`,
527
+ error
528
+ );
529
+ }
198
530
  } else {
199
- const attributeName = isSVG ? getSVGAttributeName(key) : key;
200
531
  element.setAttribute(attributeName, String(value));
201
532
  }
202
- } else {
203
- const attributeName = isSVG ? getSVGAttributeName(key) : key;
204
- element.setAttribute(attributeName, String(value));
205
533
  }
206
- });
534
+ }
535
+ } else {
536
+ const attributeName = isSVG ? getSVGAttributeName(key) : key;
537
+ if (typeof value === "object" && value !== null) {
538
+ try {
539
+ const serialized = JSON.stringify(value);
540
+ if (serialized.length > 1024 * 1024) {
541
+ logger2.warn(
542
+ `[WSX] Property "${key}" value too large for attribute, consider using a JavaScript property instead`
543
+ );
544
+ }
545
+ element.setAttribute(attributeName, serialized);
546
+ } catch (error) {
547
+ logger2.warn(`Cannot serialize property "${key}" for attribute:`, error);
548
+ }
549
+ } else {
550
+ element.setAttribute(attributeName, String(value));
551
+ }
207
552
  }
553
+ }
554
+
555
+ // src/utils/element-creation.ts
556
+ function applySingleProp(element, key, value, tag, isSVG) {
557
+ if (value === null || value === void 0 || value === false) {
558
+ return;
559
+ }
560
+ if (key === "ref" && typeof value === "function") {
561
+ value(element);
562
+ return;
563
+ }
564
+ if (key === "className" || key === "class") {
565
+ if (isSVG) {
566
+ element.setAttribute("class", value);
567
+ } else {
568
+ element.className = value;
569
+ }
570
+ return;
571
+ }
572
+ if (key === "style" && typeof value === "string") {
573
+ element.setAttribute("style", value);
574
+ return;
575
+ }
576
+ if (key.startsWith("on") && typeof value === "function") {
577
+ const eventName = key.slice(2).toLowerCase();
578
+ element.addEventListener(eventName, value);
579
+ return;
580
+ }
581
+ if (typeof value === "boolean") {
582
+ if (value) {
583
+ element.setAttribute(key, "");
584
+ }
585
+ return;
586
+ }
587
+ if (key === "value") {
588
+ if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) {
589
+ element.value = String(value);
590
+ } else {
591
+ const attributeName = isSVG ? getSVGAttributeName(key) : key;
592
+ element.setAttribute(attributeName, String(value));
593
+ }
594
+ return;
595
+ }
596
+ if (isFrameworkInternalProp(key)) {
597
+ return;
598
+ }
599
+ setSmartProperty(element, key, value, tag);
600
+ }
601
+ function applyPropsToElement(element, props, tag) {
602
+ if (!props) {
603
+ return;
604
+ }
605
+ const isSVG = shouldUseSVGNamespace(tag);
606
+ Object.entries(props).forEach(([key, value]) => {
607
+ applySingleProp(element, key, value, tag, isSVG);
608
+ });
609
+ }
610
+ function appendChildrenToElement(element, children) {
208
611
  const flatChildren = flattenChildren(children);
209
612
  flatChildren.forEach((child) => {
210
613
  if (child === null || child === void 0 || child === false) {
@@ -218,60 +621,278 @@ function h(tag, props = {}, ...children) {
218
621
  element.appendChild(child);
219
622
  }
220
623
  });
624
+ }
625
+ function createElementWithPropsAndChildren(tag, props, children) {
626
+ const element = createElement(tag);
627
+ applyPropsToElement(element, props, tag);
628
+ appendChildrenToElement(element, children);
221
629
  return element;
222
630
  }
223
- function isHTMLString(str) {
224
- const trimmed = str.trim();
225
- if (!trimmed) return false;
226
- const htmlTagPattern = /<[a-z][a-z0-9]*(\s[^>]*)?(\/>|>)/i;
227
- const looksLikeMath = /^[^<]*<[^>]*>[^>]*$/.test(trimmed) && !htmlTagPattern.test(trimmed);
228
- if (looksLikeMath) return false;
229
- return htmlTagPattern.test(trimmed);
631
+
632
+ // src/utils/element-update.ts
633
+ function removeProp(element, key, oldValue, tag) {
634
+ const isSVG = shouldUseSVGNamespace(tag);
635
+ if (key === "ref") {
636
+ return;
637
+ }
638
+ if (key === "className" || key === "class") {
639
+ if (isSVG) {
640
+ element.removeAttribute("class");
641
+ } else {
642
+ element.className = "";
643
+ }
644
+ return;
645
+ }
646
+ if (key === "style") {
647
+ element.removeAttribute("style");
648
+ return;
649
+ }
650
+ if (key.startsWith("on") && typeof oldValue === "function") {
651
+ return;
652
+ }
653
+ if (key === "value") {
654
+ if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) {
655
+ element.value = "";
656
+ } else {
657
+ const attributeName2 = isSVG ? getSVGAttributeName(key) : key;
658
+ element.removeAttribute(attributeName2);
659
+ }
660
+ return;
661
+ }
662
+ if (isFrameworkInternalProp(key)) {
663
+ return;
664
+ }
665
+ const attributeName = isSVG ? getSVGAttributeName(key) : key;
666
+ element.removeAttribute(attributeName);
667
+ try {
668
+ delete element[key];
669
+ } catch {
670
+ }
230
671
  }
231
- function flattenChildren(children, skipHTMLDetection = false, depth = 0) {
232
- if (depth > 10) {
233
- console.warn(
234
- "[WSX] flattenChildren: Maximum depth exceeded, treating remaining children as text"
235
- );
236
- return children.filter(
237
- (child) => typeof child === "string" || typeof child === "number"
238
- );
672
+ function applySingleProp2(element, key, value, tag, isSVG) {
673
+ if (value === null || value === void 0 || value === false) {
674
+ return;
239
675
  }
240
- const result = [];
241
- for (const child of children) {
242
- if (child === null || child === void 0 || child === false) {
676
+ if (key === "ref" && typeof value === "function") {
677
+ value(element);
678
+ return;
679
+ }
680
+ if (key === "className" || key === "class") {
681
+ if (isSVG) {
682
+ element.setAttribute("class", value);
683
+ } else {
684
+ element.className = value;
685
+ }
686
+ return;
687
+ }
688
+ if (key === "style" && typeof value === "string") {
689
+ element.setAttribute("style", value);
690
+ return;
691
+ }
692
+ if (key.startsWith("on") && typeof value === "function") {
693
+ const eventName = key.slice(2).toLowerCase();
694
+ element.addEventListener(eventName, value);
695
+ return;
696
+ }
697
+ if (typeof value === "boolean") {
698
+ if (value) {
699
+ element.setAttribute(key, "");
700
+ }
701
+ return;
702
+ }
703
+ if (key === "value") {
704
+ if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) {
705
+ element.value = String(value);
706
+ } else {
707
+ const attributeName = isSVG ? getSVGAttributeName(key) : key;
708
+ element.setAttribute(attributeName, String(value));
709
+ }
710
+ return;
711
+ }
712
+ if (isFrameworkInternalProp(key)) {
713
+ return;
714
+ }
715
+ setSmartProperty(element, key, value, tag);
716
+ }
717
+ function updateProps(element, oldProps, newProps, tag) {
718
+ const isSVG = shouldUseSVGNamespace(tag);
719
+ const old = oldProps || {};
720
+ const new_ = newProps || {};
721
+ for (const key in old) {
722
+ if (!(key in new_)) {
723
+ removeProp(element, key, old[key], tag);
724
+ }
725
+ }
726
+ for (const key in new_) {
727
+ const oldValue = old[key];
728
+ const newValue = new_[key];
729
+ if (oldValue === newValue) {
243
730
  continue;
244
- } else if (Array.isArray(child)) {
245
- result.push(...flattenChildren(child, skipHTMLDetection, depth + 1));
246
- } else if (typeof child === "string") {
247
- if (skipHTMLDetection) {
248
- result.push(child);
249
- } else if (isHTMLString(child)) {
250
- try {
251
- const nodes = parseHTMLToNodes(child);
252
- if (nodes.length > 0) {
253
- for (const node of nodes) {
254
- if (typeof node === "string") {
255
- result.push(node);
256
- } else {
257
- result.push(node);
258
- }
731
+ }
732
+ if (typeof oldValue === "object" && oldValue !== null && typeof newValue === "object" && newValue !== null) {
733
+ if (JSON.stringify(oldValue) === JSON.stringify(newValue)) {
734
+ continue;
735
+ }
736
+ }
737
+ applySingleProp2(element, key, newValue, tag, isSVG);
738
+ }
739
+ }
740
+ function updateChildren(element, oldChildren, newChildren) {
741
+ const flatOld = flattenChildren(oldChildren);
742
+ const flatNew = flattenChildren(newChildren);
743
+ const minLength = Math.min(flatOld.length, flatNew.length);
744
+ for (let i = 0; i < minLength; i++) {
745
+ const oldChild = flatOld[i];
746
+ const newChild = flatNew[i];
747
+ if (typeof oldChild === "string" || typeof oldChild === "number") {
748
+ if (typeof newChild === "string" || typeof newChild === "number") {
749
+ const textNode = element.childNodes[i];
750
+ if (textNode && textNode.nodeType === Node.TEXT_NODE) {
751
+ textNode.textContent = String(newChild);
752
+ } else {
753
+ const newTextNode = document.createTextNode(String(newChild));
754
+ if (textNode) {
755
+ element.replaceChild(newTextNode, textNode);
756
+ } else {
757
+ element.appendChild(newTextNode);
758
+ }
759
+ }
760
+ } else {
761
+ const textNode = element.childNodes[i];
762
+ if (textNode) {
763
+ if (!shouldPreserveElement(textNode)) {
764
+ element.removeChild(textNode);
765
+ }
766
+ }
767
+ if (typeof newChild === "string" || typeof newChild === "number") {
768
+ element.appendChild(document.createTextNode(String(newChild)));
769
+ } else if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
770
+ element.appendChild(newChild);
771
+ } else if (newChild instanceof DocumentFragment) {
772
+ element.appendChild(newChild);
773
+ }
774
+ }
775
+ } else if (oldChild instanceof HTMLElement || oldChild instanceof SVGElement) {
776
+ if (newChild === oldChild) {
777
+ continue;
778
+ } else if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
779
+ const oldNode = element.childNodes[i];
780
+ if (oldNode) {
781
+ if (!shouldPreserveElement(oldNode)) {
782
+ if (oldNode !== newChild) {
783
+ element.replaceChild(newChild, oldNode);
259
784
  }
260
785
  } else {
261
- result.push(child);
786
+ if (newChild.parentNode !== element) {
787
+ element.appendChild(newChild);
788
+ }
789
+ }
790
+ } else {
791
+ if (newChild.parentNode !== element) {
792
+ element.appendChild(newChild);
262
793
  }
263
- } catch (error) {
264
- console.warn("[WSX] Failed to parse HTML string, treating as text:", error);
265
- result.push(child);
266
794
  }
267
795
  } else {
268
- result.push(child);
796
+ const oldNode = element.childNodes[i];
797
+ if (oldNode) {
798
+ if (!shouldPreserveElement(oldNode)) {
799
+ element.removeChild(oldNode);
800
+ }
801
+ }
802
+ if (typeof newChild === "string" || typeof newChild === "number") {
803
+ element.appendChild(document.createTextNode(String(newChild)));
804
+ } else if (newChild instanceof DocumentFragment) {
805
+ element.appendChild(newChild);
806
+ }
269
807
  }
270
- } else {
271
- result.push(child);
272
808
  }
273
809
  }
274
- return result;
810
+ for (let i = minLength; i < flatNew.length; i++) {
811
+ const newChild = flatNew[i];
812
+ if (newChild === null || newChild === void 0 || newChild === false) {
813
+ continue;
814
+ }
815
+ if (typeof newChild === "string" || typeof newChild === "number") {
816
+ element.appendChild(document.createTextNode(String(newChild)));
817
+ } else if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
818
+ if (newChild.parentNode !== element) {
819
+ element.appendChild(newChild);
820
+ }
821
+ } else if (newChild instanceof DocumentFragment) {
822
+ element.appendChild(newChild);
823
+ }
824
+ }
825
+ const nodesToRemove = [];
826
+ for (let i = flatNew.length; i < element.childNodes.length; i++) {
827
+ const child = element.childNodes[i];
828
+ if (!shouldPreserveElement(child)) {
829
+ nodesToRemove.push(child);
830
+ }
831
+ }
832
+ for (let i = nodesToRemove.length - 1; i >= 0; i--) {
833
+ const node = nodesToRemove[i];
834
+ if (node.parentNode === element) {
835
+ element.removeChild(node);
836
+ }
837
+ }
838
+ }
839
+ function updateElement(element, newProps, newChildren, tag, cacheManager) {
840
+ const oldMetadata = cacheManager.getMetadata(element);
841
+ const oldProps = oldMetadata?.props || null;
842
+ const oldChildren = oldMetadata?.children || [];
843
+ updateProps(element, oldProps, newProps, tag);
844
+ updateChildren(element, oldChildren, newChildren);
845
+ cacheManager.setMetadata(element, {
846
+ props: newProps || {},
847
+ children: newChildren
848
+ });
849
+ }
850
+
851
+ // src/jsx-factory.ts
852
+ var logger3 = createLogger("JSX Factory");
853
+ function h(tag, props = {}, ...children) {
854
+ if (typeof tag === "function") {
855
+ return tag(props, children);
856
+ }
857
+ const context = RenderContext.getCurrentComponent();
858
+ const cacheManager = context ? RenderContext.getDOMCache() : null;
859
+ if (context && cacheManager) {
860
+ return tryUseCacheOrCreate(tag, props, children, context, cacheManager);
861
+ }
862
+ return createElementWithPropsAndChildren(tag, props, children);
863
+ }
864
+ function tryUseCacheOrCreate(tag, props, children, context, cacheManager) {
865
+ try {
866
+ const componentId = getComponentId();
867
+ const cacheKey = generateCacheKey(tag, props, componentId, context);
868
+ const cachedElement = cacheManager.get(cacheKey);
869
+ if (cachedElement) {
870
+ const element2 = cachedElement;
871
+ updateElement(element2, props, children, tag, cacheManager);
872
+ return element2;
873
+ }
874
+ const element = createElementWithPropsAndChildren(tag, props, children);
875
+ cacheManager.set(cacheKey, element);
876
+ markElement(element, cacheKey);
877
+ cacheManager.setMetadata(element, {
878
+ props: props || {},
879
+ children
880
+ });
881
+ return element;
882
+ } catch (error) {
883
+ return handleCacheError(error, tag, props, children);
884
+ }
885
+ }
886
+ function handleCacheError(error, tag, props, children) {
887
+ try {
888
+ const nodeEnv = typeof globalThis.process !== "undefined" && // eslint-disable-next-line @typescript-eslint/no-explicit-any
889
+ globalThis.process.env?.NODE_ENV;
890
+ if (nodeEnv === "development") {
891
+ logger3.warn("[WSX DOM Cache] Cache error, falling back to create new element:", error);
892
+ }
893
+ } catch {
894
+ }
895
+ return createElementWithPropsAndChildren(tag, props, children);
275
896
  }
276
897
  function Fragment(_props, children) {
277
898
  const fragment = document.createDocumentFragment();
@@ -333,7 +954,7 @@ StyleManager.styleSheets = /* @__PURE__ */ new Map();
333
954
 
334
955
  // src/utils/reactive.ts
335
956
  var import_wsx_logger = require("@wsxjs/wsx-logger");
336
- var logger = (0, import_wsx_logger.createLogger)("ReactiveSystem");
957
+ var logger4 = (0, import_wsx_logger.createLogger)("ReactiveSystem");
337
958
  var UpdateScheduler = class {
338
959
  constructor() {
339
960
  this.pendingCallbacks = /* @__PURE__ */ new Set();
@@ -362,7 +983,7 @@ var UpdateScheduler = class {
362
983
  try {
363
984
  callback();
364
985
  } catch (error) {
365
- logger.error("[WSX Reactive] Error in callback:", error);
986
+ logger4.error("[WSX Reactive] Error in callback:", error);
366
987
  }
367
988
  });
368
989
  }
@@ -504,7 +1125,7 @@ var ReactiveDebug = {
504
1125
  */
505
1126
  log(message, ...args) {
506
1127
  if (this.isEnabled()) {
507
- logger.info(`[WSX Reactive] ${message}`, ...args);
1128
+ logger4.info(`[WSX Reactive] ${message}`, ...args);
508
1129
  }
509
1130
  }
510
1131
  };
@@ -552,6 +1173,108 @@ function reactiveWithDebug(obj, onChange, debugName) {
552
1173
  });
553
1174
  }
554
1175
 
1176
+ // src/dom-cache-manager.ts
1177
+ var logger5 = createLogger("DOMCacheManager");
1178
+ var DOMCacheManager = class {
1179
+ constructor() {
1180
+ // Map<CacheKey, DOMElement>
1181
+ this.cache = /* @__PURE__ */ new Map();
1182
+ // Map<DOMElement, Metadata>
1183
+ // Stores metadata (props, children) for cached elements to support diffing
1184
+ this.metadata = /* @__PURE__ */ new WeakMap();
1185
+ // Track key-parent relationships to detect duplicate keys in all environments
1186
+ // Map<CacheKey, ParentInfo>
1187
+ this.keyParentMap = /* @__PURE__ */ new Map();
1188
+ // Flag to enable duplicate key warnings (enabled by default, critical for correctness)
1189
+ this.warnDuplicateKeys = true;
1190
+ }
1191
+ /**
1192
+ * Retrieves an element from the cache.
1193
+ * @param key The unique cache key.
1194
+ */
1195
+ get(key) {
1196
+ return this.cache.get(key);
1197
+ }
1198
+ /**
1199
+ * Stores an element in the cache.
1200
+ * @param key The unique cache key.
1201
+ * @param element The DOM element to cache.
1202
+ */
1203
+ set(key, element) {
1204
+ if (this.warnDuplicateKeys) {
1205
+ this.checkDuplicateKey(key, element);
1206
+ }
1207
+ this.cache.set(key, element);
1208
+ }
1209
+ /**
1210
+ * Checks if a cache key is being reused in a different parent container.
1211
+ * Runs in all environments to help developers catch key conflicts early.
1212
+ * This is critical for correctness and helps prevent subtle bugs.
1213
+ */
1214
+ checkDuplicateKey(key, element) {
1215
+ const existing = this.keyParentMap.get(key);
1216
+ const currentParent = element.parentElement;
1217
+ if (existing && currentParent) {
1218
+ const currentParentInfo = this.getParentInfo(currentParent);
1219
+ const existingParentInfo = `${existing.parentTag}${existing.parentClass ? "." + existing.parentClass : ""}`;
1220
+ if (currentParentInfo !== existingParentInfo) {
1221
+ logger5.warn(
1222
+ `Duplicate key "${key}" detected in different parent containers!
1223
+ Previous parent: ${existingParentInfo}
1224
+ Current parent: ${currentParentInfo}
1225
+
1226
+ This may cause elements to appear in wrong containers or be moved unexpectedly.
1227
+
1228
+ Solution: Use unique key prefixes for different locations:
1229
+ Example: <wsx-link key="nav-0"> vs <wsx-link key="overflow-0">
1230
+
1231
+ See https://wsxjs.dev/docs/guide/DOM_CACHE_GUIDE for best practices.`
1232
+ );
1233
+ }
1234
+ }
1235
+ if (currentParent) {
1236
+ this.keyParentMap.set(key, {
1237
+ parentTag: currentParent.tagName.toLowerCase(),
1238
+ parentClass: currentParent.className,
1239
+ element
1240
+ });
1241
+ }
1242
+ }
1243
+ /**
1244
+ * Gets a formatted parent container description.
1245
+ */
1246
+ getParentInfo(parent) {
1247
+ const tag = parent.tagName.toLowerCase();
1248
+ const className = parent.className;
1249
+ return `${tag}${className ? "." + className.split(" ")[0] : ""}`;
1250
+ }
1251
+ /**
1252
+ * Checks if a key exists in the cache.
1253
+ */
1254
+ has(key) {
1255
+ return this.cache.has(key);
1256
+ }
1257
+ /**
1258
+ * Clears the cache.
1259
+ * Should be called when component is disconnected or cache is invalidated.
1260
+ */
1261
+ clear() {
1262
+ this.cache.clear();
1263
+ }
1264
+ /**
1265
+ * Stores metadata for an element (e.g. previous props).
1266
+ */
1267
+ setMetadata(element, meta) {
1268
+ this.metadata.set(element, meta);
1269
+ }
1270
+ /**
1271
+ * Retrieves metadata for an element.
1272
+ */
1273
+ getMetadata(element) {
1274
+ return this.metadata.get(element);
1275
+ }
1276
+ };
1277
+
555
1278
  // src/base-component.ts
556
1279
  var BaseComponent = class extends HTMLElement {
557
1280
  constructor(config = {}) {
@@ -559,6 +1282,11 @@ var BaseComponent = class extends HTMLElement {
559
1282
  this.connected = false;
560
1283
  this._isDebugEnabled = false;
561
1284
  this._reactiveStates = /* @__PURE__ */ new Map();
1285
+ /**
1286
+ * DOM Cache Manager for fine-grained updates (RFC 0037)
1287
+ * @internal
1288
+ */
1289
+ this._domCache = new DOMCacheManager();
562
1290
  /**
563
1291
  * 当前捕获的焦点状态(用于在 render 时使用捕获的值)
564
1292
  * @internal - 由 rerender() 方法管理
@@ -623,6 +1351,13 @@ var BaseComponent = class extends HTMLElement {
623
1351
  static get observedAttributes() {
624
1352
  return [];
625
1353
  }
1354
+ /**
1355
+ * Gets the DOMCacheManager instance.
1356
+ * @internal
1357
+ */
1358
+ getDomCache() {
1359
+ return this._domCache;
1360
+ }
626
1361
  /**
627
1362
  * Web Component生命周期:属性变化
628
1363
  */
@@ -930,7 +1665,7 @@ var BaseComponent = class extends HTMLElement {
930
1665
 
931
1666
  // src/web-component.ts
932
1667
  var import_wsx_logger2 = require("@wsxjs/wsx-logger");
933
- var logger2 = (0, import_wsx_logger2.createLogger)("WebComponent");
1668
+ var logger6 = (0, import_wsx_logger2.createLogger)("WebComponent");
934
1669
  var WebComponent = class extends BaseComponent {
935
1670
  // Initialized by BaseComponent constructor
936
1671
  constructor(config = {}) {
@@ -979,7 +1714,7 @@ var WebComponent = class extends BaseComponent {
979
1714
  });
980
1715
  }
981
1716
  } catch (error) {
982
- logger2.error(`Error in connectedCallback:`, error);
1717
+ logger6.error(`Error in connectedCallback:`, error);
983
1718
  this.renderError(error);
984
1719
  }
985
1720
  }
@@ -1032,7 +1767,7 @@ var WebComponent = class extends BaseComponent {
1032
1767
  StyleManager.applyStyles(this.shadowRoot, styleName, stylesToApply);
1033
1768
  }
1034
1769
  }
1035
- const content = this.render();
1770
+ const content = RenderContext.runInContext(this, () => this.render());
1036
1771
  if (focusState && focusState.key && focusState.value !== void 0) {
1037
1772
  const target = content.querySelector(
1038
1773
  `[data-wsx-key="${focusState.key}"]`
@@ -1060,7 +1795,7 @@ var WebComponent = class extends BaseComponent {
1060
1795
  });
1061
1796
  });
1062
1797
  } catch (error) {
1063
- logger2.error("Error in _rerender:", error);
1798
+ logger6.error("Error in _rerender:", error);
1064
1799
  this.renderError(error);
1065
1800
  this._isRendering = false;
1066
1801
  }
@@ -1088,7 +1823,7 @@ var WebComponent = class extends BaseComponent {
1088
1823
 
1089
1824
  // src/light-component.ts
1090
1825
  var import_wsx_logger3 = require("@wsxjs/wsx-logger");
1091
- var logger3 = (0, import_wsx_logger3.createLogger)("LightComponent");
1826
+ var logger7 = (0, import_wsx_logger3.createLogger)("LightComponent");
1092
1827
  var LightComponent = class extends BaseComponent {
1093
1828
  // Initialized by BaseComponent constructor
1094
1829
  constructor(config = {}) {
@@ -1130,7 +1865,7 @@ var LightComponent = class extends BaseComponent {
1130
1865
  (child) => child !== styleElement
1131
1866
  );
1132
1867
  childrenToRemove.forEach((child) => child.remove());
1133
- const content = this.render();
1868
+ const content = RenderContext.runInContext(this, () => this.render());
1134
1869
  this.appendChild(content);
1135
1870
  if (styleElement && styleElement !== this.firstChild) {
1136
1871
  this.insertBefore(styleElement, this.firstChild);
@@ -1144,7 +1879,7 @@ var LightComponent = class extends BaseComponent {
1144
1879
  });
1145
1880
  }
1146
1881
  } catch (error) {
1147
- logger3.error(`[${this.constructor.name}] Error in connectedCallback:`, error);
1882
+ logger7.error(`[${this.constructor.name}] Error in connectedCallback:`, error);
1148
1883
  this.renderError(error);
1149
1884
  }
1150
1885
  }
@@ -1192,7 +1927,7 @@ var LightComponent = class extends BaseComponent {
1192
1927
  this._pendingFocusState = focusState;
1193
1928
  const jsxChildren = this.getJSXChildren();
1194
1929
  try {
1195
- const content = this.render();
1930
+ const content = RenderContext.runInContext(this, () => this.render());
1196
1931
  if (focusState && focusState.key && focusState.value !== void 0) {
1197
1932
  const target = content.querySelector(
1198
1933
  `[data-wsx-key="${focusState.key}"]`
@@ -1249,7 +1984,7 @@ var LightComponent = class extends BaseComponent {
1249
1984
  });
1250
1985
  });
1251
1986
  } catch (error) {
1252
- logger3.error(`[${this.constructor.name}] Error in _rerender:`, error);
1987
+ logger7.error(`[${this.constructor.name}] Error in _rerender:`, error);
1253
1988
  this.renderError(error);
1254
1989
  this._isRendering = false;
1255
1990
  }
@@ -1371,6 +2106,10 @@ To fix this, please:
1371
2106
  See: https://github.com/wsxjs/wsxjs#setup for more details.`;
1372
2107
  }
1373
2108
  function state(targetOrContext, propertyKey) {
2109
+ const globalProcess = typeof globalThis !== "undefined" ? globalThis.process : void 0;
2110
+ if (globalProcess?.env?.NODE_ENV === "test") {
2111
+ return;
2112
+ }
1374
2113
  let propertyName = "unknown";
1375
2114
  const propertyKeyIsObject = typeof propertyKey === "object" && propertyKey !== null;
1376
2115
  const targetIsObject = typeof targetOrContext === "object" && targetOrContext !== null;