@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/jsx.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  Fragment,
3
3
  h
4
- } from "./chunk-UH5BDYGI.mjs";
4
+ } from "./chunk-AR3DIDLV.mjs";
5
5
  export {
6
6
  Fragment,
7
7
  h
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wsxjs/wsx-core",
3
- "version": "0.0.19",
3
+ "version": "0.0.21",
4
4
  "description": "Core WSXJS - Web Components with JSX syntax",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -48,7 +48,7 @@
48
48
  "custom-elements"
49
49
  ],
50
50
  "dependencies": {
51
- "@wsxjs/wsx-logger": "0.0.19"
51
+ "@wsxjs/wsx-logger": "0.0.21"
52
52
  },
53
53
  "devDependencies": {
54
54
  "tsup": "^8.0.0",
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import { reactive as createReactive, createState, reactiveWithDebug } from "./utils/reactive";
12
+ import { DOMCacheManager } from "./dom-cache-manager";
12
13
 
13
14
  /**
14
15
  * Type for reactive state storage
@@ -60,6 +61,12 @@ export abstract class BaseComponent extends HTMLElement {
60
61
  */
61
62
  protected _autoStyles?: string;
62
63
 
64
+ /**
65
+ * DOM Cache Manager for fine-grained updates (RFC 0037)
66
+ * @internal
67
+ */
68
+ protected _domCache = new DOMCacheManager();
69
+
63
70
  /**
64
71
  * 当前捕获的焦点状态(用于在 render 时使用捕获的值)
65
72
  * @internal - 由 rerender() 方法管理
@@ -138,6 +145,14 @@ export abstract class BaseComponent extends HTMLElement {
138
145
  */
139
146
  protected onRendered?(): void;
140
147
 
148
+ /**
149
+ * Gets the DOMCacheManager instance.
150
+ * @internal
151
+ */
152
+ public getDomCache(): DOMCacheManager {
153
+ return this._domCache;
154
+ }
155
+
141
156
  /**
142
157
  * 处理 blur 事件,在用户停止输入时执行待处理的重渲染
143
158
  * @internal
@@ -0,0 +1,135 @@
1
+ import { createLogger } from "./utils/logger";
2
+
3
+ const logger = createLogger("DOMCacheManager");
4
+
5
+ /**
6
+ * Parent container information for duplicate key detection
7
+ */
8
+ interface ParentInfo {
9
+ parentTag: string;
10
+ parentClass: string;
11
+ element: Element;
12
+ }
13
+
14
+ /**
15
+ * DOMCacheManager
16
+ *
17
+ * Manages DOM element caching for fine-grained updates (RFC 0037).
18
+ * Stores elements by unique keys derived from component ID + position/key.
19
+ */
20
+ export class DOMCacheManager {
21
+ // Map<CacheKey, DOMElement>
22
+ private cache = new Map<string, Element>();
23
+
24
+ // Map<DOMElement, Metadata>
25
+ // Stores metadata (props, children) for cached elements to support diffing
26
+ private metadata = new WeakMap<Element, Record<string, unknown>>();
27
+
28
+ // Track key-parent relationships to detect duplicate keys in all environments
29
+ // Map<CacheKey, ParentInfo>
30
+ private keyParentMap = new Map<string, ParentInfo>();
31
+
32
+ // Flag to enable duplicate key warnings (enabled by default, critical for correctness)
33
+ private warnDuplicateKeys = true;
34
+
35
+ /**
36
+ * Retrieves an element from the cache.
37
+ * @param key The unique cache key.
38
+ */
39
+ get(key: string): Element | undefined {
40
+ return this.cache.get(key);
41
+ }
42
+
43
+ /**
44
+ * Stores an element in the cache.
45
+ * @param key The unique cache key.
46
+ * @param element The DOM element to cache.
47
+ */
48
+ set(key: string, element: Element): void {
49
+ // Always check for duplicate keys (critical for correctness)
50
+ if (this.warnDuplicateKeys) {
51
+ this.checkDuplicateKey(key, element);
52
+ }
53
+
54
+ this.cache.set(key, element);
55
+ }
56
+
57
+ /**
58
+ * Checks if a cache key is being reused in a different parent container.
59
+ * Runs in all environments to help developers catch key conflicts early.
60
+ * This is critical for correctness and helps prevent subtle bugs.
61
+ */
62
+ private checkDuplicateKey(key: string, element: Element): void {
63
+ const existing = this.keyParentMap.get(key);
64
+ const currentParent = element.parentElement;
65
+
66
+ if (existing && currentParent) {
67
+ const currentParentInfo = this.getParentInfo(currentParent);
68
+ const existingParentInfo = `${existing.parentTag}${existing.parentClass ? "." + existing.parentClass : ""}`;
69
+
70
+ // Check if the element is being used in a different parent container
71
+ if (currentParentInfo !== existingParentInfo) {
72
+ logger.warn(
73
+ `Duplicate key "${key}" detected in different parent containers!\n` +
74
+ ` Previous parent: ${existingParentInfo}\n` +
75
+ ` Current parent: ${currentParentInfo}\n` +
76
+ `\n` +
77
+ `This may cause elements to appear in wrong containers or be moved unexpectedly.\n` +
78
+ `\n` +
79
+ `Solution: Use unique key prefixes for different locations:\n` +
80
+ ` Example: <wsx-link key="nav-0"> vs <wsx-link key="overflow-0">\n` +
81
+ `\n` +
82
+ `See https://wsxjs.dev/docs/guide/DOM_CACHE_GUIDE for best practices.`
83
+ );
84
+ }
85
+ }
86
+
87
+ // Track this key-parent relationship for future checks
88
+ if (currentParent) {
89
+ this.keyParentMap.set(key, {
90
+ parentTag: currentParent.tagName.toLowerCase(),
91
+ parentClass: currentParent.className,
92
+ element,
93
+ });
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Gets a formatted parent container description.
99
+ */
100
+ private getParentInfo(parent: Element): string {
101
+ const tag = parent.tagName.toLowerCase();
102
+ const className = parent.className;
103
+ return `${tag}${className ? "." + className.split(" ")[0] : ""}`;
104
+ }
105
+
106
+ /**
107
+ * Checks if a key exists in the cache.
108
+ */
109
+ has(key: string): boolean {
110
+ return this.cache.has(key);
111
+ }
112
+
113
+ /**
114
+ * Clears the cache.
115
+ * Should be called when component is disconnected or cache is invalidated.
116
+ */
117
+ clear(): void {
118
+ this.cache.clear();
119
+ // WeakMap doesn't need clearing
120
+ }
121
+
122
+ /**
123
+ * Stores metadata for an element (e.g. previous props).
124
+ */
125
+ setMetadata(element: Element, meta: Record<string, unknown>): void {
126
+ this.metadata.set(element, meta);
127
+ }
128
+
129
+ /**
130
+ * Retrieves metadata for an element.
131
+ */
132
+ getMetadata(element: Element): Record<string, unknown> | undefined {
133
+ return this.metadata.get(element);
134
+ }
135
+ }
@@ -11,20 +11,18 @@
11
11
 
12
12
  // JSX 类型声明已移至 types/wsx-types.d.ts
13
13
 
14
- import { createElement, shouldUseSVGNamespace, getSVGAttributeName } from "./utils/svg-utils";
15
- import { parseHTMLToNodes } from "./utils/dom-utils";
16
-
17
- // JSX子元素类型
18
- export type JSXChildren =
19
- | string
20
- | number
21
- | HTMLElement
22
- | SVGElement
23
- | DocumentFragment
24
- | JSXChildren[]
25
- | null
26
- | undefined
27
- | boolean;
14
+ import { flattenChildren, type JSXChildren } from "./utils/dom-utils";
15
+ import { generateCacheKey, getComponentId } from "./utils/cache-key";
16
+ import { markElement } from "./utils/element-marking";
17
+ import { RenderContext } from "./render-context";
18
+ import { createElementWithPropsAndChildren } from "./utils/element-creation";
19
+ import { updateElement } from "./utils/element-update";
20
+ import type { BaseComponent } from "./base-component";
21
+ import type { DOMCacheManager } from "./dom-cache-manager";
22
+ import { createLogger } from "./utils/logger";
23
+ const logger = createLogger("JSX Factory");
24
+ // JSX子元素类型(从 dom-utils 重新导出以保持向后兼容)
25
+ export type { JSXChildren } from "./utils/dom-utils";
28
26
 
29
27
  /**
30
28
  * 纯原生JSX工厂函数
@@ -44,200 +42,85 @@ export function h(
44
42
  props: Record<string, unknown> | null = {},
45
43
  ...children: JSXChildren[]
46
44
  ): HTMLElement | SVGElement {
47
- // 处理组件函数
45
+ // 处理组件函数(不受缓存影响)
48
46
  if (typeof tag === "function") {
49
47
  return tag(props, children);
50
48
  }
51
49
 
52
- // 创建DOM元素 - 自动检测SVG命名空间
53
- const element = createElement(tag);
54
-
55
- // 处理属性
56
- if (props) {
57
- const isSVG = shouldUseSVGNamespace(tag);
50
+ // 检查上下文(阶段 3.2:启用缓存机制)
51
+ const context = RenderContext.getCurrentComponent();
52
+ const cacheManager = context ? RenderContext.getDOMCache() : null;
58
53
 
59
- Object.entries(props).forEach(([key, value]) => {
60
- if (value === null || value === undefined || value === false) {
61
- return;
62
- }
63
-
64
- // 处理ref回调
65
- if (key === "ref" && typeof value === "function") {
66
- value(element);
67
- }
68
- // 处理className和class
69
- else if (key === "className" || key === "class") {
70
- if (isSVG) {
71
- // SVG元素使用class属性
72
- element.setAttribute("class", value as string);
73
- } else {
74
- // HTML元素可以使用className
75
- (element as HTMLElement).className = value as string;
76
- }
77
- }
78
- // 处理style
79
- else if (key === "style" && typeof value === "string") {
80
- element.setAttribute("style", value);
81
- }
82
- // 处理事件监听器
83
- else if (key.startsWith("on") && typeof value === "function") {
84
- const eventName = key.slice(2).toLowerCase();
85
- element.addEventListener(eventName, value as EventListener);
86
- }
87
- // 处理布尔属性
88
- else if (typeof value === "boolean") {
89
- if (value) {
90
- element.setAttribute(key, "");
91
- }
92
- }
93
- // 特殊处理 input/textarea/select 的 value 属性
94
- // 使用 .value 而不是 setAttribute,因为 .value 是当前值,setAttribute 是初始值
95
- else if (key === "value") {
96
- if (
97
- element instanceof HTMLInputElement ||
98
- element instanceof HTMLTextAreaElement ||
99
- element instanceof HTMLSelectElement
100
- ) {
101
- element.value = String(value);
102
- } else {
103
- // 对于其他元素,使用 setAttribute
104
- const attributeName = isSVG ? getSVGAttributeName(key) : key;
105
- element.setAttribute(attributeName, String(value));
106
- }
107
- }
108
- // 处理其他属性
109
- else {
110
- // 对SVG元素使用正确的属性名
111
- const attributeName = isSVG ? getSVGAttributeName(key) : key;
112
- element.setAttribute(attributeName, String(value));
113
- }
114
- });
54
+ if (context && cacheManager) {
55
+ return tryUseCacheOrCreate(tag, props, children, context, cacheManager);
115
56
  }
116
57
 
117
- // 处理子元素
118
- const flatChildren = flattenChildren(children);
119
- flatChildren.forEach((child) => {
120
- if (child === null || child === undefined || child === false) {
121
- return;
122
- }
123
-
124
- if (typeof child === "string" || typeof child === "number") {
125
- element.appendChild(document.createTextNode(String(child)));
126
- } else if (child instanceof HTMLElement || child instanceof SVGElement) {
127
- element.appendChild(child);
128
- } else if (child instanceof DocumentFragment) {
129
- element.appendChild(child);
130
- }
131
- });
132
-
133
- return element;
58
+ // 无上下文:使用旧逻辑(向后兼容)
59
+ return createElementWithPropsAndChildren(tag, props, children);
134
60
  }
135
61
 
136
62
  /**
137
- * 检测字符串是否包含HTML标签
138
- * 使用更严格的检测:必须包含完整的 HTML 标签(开始和结束标签,或自闭合标签)
63
+ * Tries to use cached element or creates a new one.
139
64
  */
140
- function isHTMLString(str: string): boolean {
141
- const trimmed = str.trim();
142
- if (!trimmed) return false;
143
-
144
- // 更严格的检测:必须包含完整的 HTML 标签
145
- // 1. 必须以 < 开头
146
- // 2. 后面跟着字母(标签名)
147
- // 3. 必须包含 > 来闭合标签
148
- // 4. 排除单独的 < 或 > 符号(如数学表达式 "a < b")
149
- const htmlTagPattern = /<[a-z][a-z0-9]*(\s[^>]*)?(\/>|>)/i;
150
-
151
- // 额外检查:确保不是纯文本中的 < 和 >(如 "a < b" 或 "x > y")
152
- // 如果字符串看起来像数学表达式或纯文本,不应该被检测为 HTML
153
- const looksLikeMath = /^[^<]*<[^>]*>[^>]*$/.test(trimmed) && !htmlTagPattern.test(trimmed);
154
- if (looksLikeMath) return false;
65
+ function tryUseCacheOrCreate(
66
+ tag: string,
67
+ props: Record<string, unknown> | null,
68
+ children: JSXChildren[],
69
+ context: BaseComponent,
70
+ cacheManager: DOMCacheManager
71
+ ): HTMLElement | SVGElement {
72
+ try {
73
+ const componentId = getComponentId();
74
+ const cacheKey = generateCacheKey(tag, props, componentId, context);
75
+ const cachedElement = cacheManager.get(cacheKey);
76
+
77
+ if (cachedElement) {
78
+ // 缓存命中:复用元素并更新内容(阶段 4:细粒度更新)
79
+ // 缓存键已经确保了唯一性(componentId + tag + position/key/index)
80
+ // 不需要再检查标签名(可能导致错误复用)
81
+ const element = cachedElement as HTMLElement | SVGElement;
82
+ updateElement(element, props, children, tag, cacheManager);
83
+ return element;
84
+ }
155
85
 
156
- return htmlTagPattern.test(trimmed);
86
+ // ❌ 缓存未命中:创建新元素
87
+ const element = createElementWithPropsAndChildren(tag, props, children);
88
+ cacheManager.set(cacheKey, element);
89
+ markElement(element, cacheKey);
90
+ // 保存初始元数据(用于下次更新)
91
+ cacheManager.setMetadata(element, {
92
+ props: props || {},
93
+ children: children,
94
+ });
95
+ return element;
96
+ } catch (error) {
97
+ // 缓存失败:降级到创建新元素
98
+ return handleCacheError(error, tag, props, children);
99
+ }
157
100
  }
158
101
 
159
102
  /**
160
- * 扁平化子元素数组
161
- * 自动检测HTML字符串并转换为DOM节点
162
- *
163
- * @param children - 子元素数组
164
- * @param skipHTMLDetection - 是否跳过HTML检测(用于已解析的节点,避免无限递归)
165
- * @param depth - 当前递归深度(防止无限递归,最大深度为 10)
103
+ * Handles cache errors by logging and falling back to creating a new element.
166
104
  */
167
- function flattenChildren(
168
- children: JSXChildren[],
169
- skipHTMLDetection: boolean = false,
170
- depth: number = 0
171
- ): (string | number | HTMLElement | SVGElement | DocumentFragment | boolean | null | undefined)[] {
172
- // 防止无限递归:如果深度超过 10,停止处理
173
- if (depth > 10) {
174
- console.warn(
175
- "[WSX] flattenChildren: Maximum depth exceeded, treating remaining children as text"
176
- );
177
- return children.filter(
178
- (child): child is string | number =>
179
- typeof child === "string" || typeof child === "number"
180
- );
181
- }
182
- const result: (
183
- | string
184
- | number
185
- | HTMLElement
186
- | SVGElement
187
- | DocumentFragment
188
- | boolean
189
- | null
190
- | undefined
191
- )[] = [];
192
-
193
- for (const child of children) {
194
- if (child === null || child === undefined || child === false) {
195
- continue;
196
- } else if (Array.isArray(child)) {
197
- // 递归处理数组,保持 skipHTMLDetection 状态,增加深度
198
- result.push(...flattenChildren(child, skipHTMLDetection, depth + 1));
199
- } else if (typeof child === "string") {
200
- // 如果跳过HTML检测,直接添加字符串(避免无限递归)
201
- if (skipHTMLDetection) {
202
- result.push(child);
203
- } else if (isHTMLString(child)) {
204
- // 自动检测HTML字符串并转换为DOM节点
205
- // 使用 try-catch 防止解析失败导致崩溃
206
- try {
207
- const nodes = parseHTMLToNodes(child);
208
- // 递归处理转换后的节点数组,标记为已解析,避免再次检测HTML
209
- // parseHTMLToNodes 返回的字符串是纯文本节点,不应该再次被检测为HTML
210
- // 但是为了安全,我们仍然设置 skipHTMLDetection = true
211
- if (nodes.length > 0) {
212
- // 直接添加解析后的节点,不再递归处理(避免无限递归)
213
- // parseHTMLToNodes 已经完成了所有解析工作
214
- for (const node of nodes) {
215
- if (typeof node === "string") {
216
- // 文本节点直接添加,不再检测 HTML(已解析)
217
- result.push(node);
218
- } else {
219
- // DOM 元素直接添加
220
- result.push(node);
221
- }
222
- }
223
- } else {
224
- // 如果解析失败,回退到纯文本
225
- result.push(child);
226
- }
227
- } catch (error) {
228
- // 如果解析失败,回退到纯文本,避免崩溃
229
- console.warn("[WSX] Failed to parse HTML string, treating as text:", error);
230
- result.push(child);
231
- }
232
- } else {
233
- result.push(child);
234
- }
235
- } else {
236
- result.push(child);
105
+ function handleCacheError(
106
+ error: unknown,
107
+ tag: string,
108
+ props: Record<string, unknown> | null,
109
+ children: JSXChildren[]
110
+ ): HTMLElement | SVGElement {
111
+ // 在开发环境输出警告,帮助调试
112
+ try {
113
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
114
+ const nodeEnv = (typeof (globalThis as any).process !== "undefined" &&
115
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
116
+ (globalThis as any).process.env?.NODE_ENV) as string | undefined;
117
+ if (nodeEnv === "development") {
118
+ logger.warn("[WSX DOM Cache] Cache error, falling back to create new element:", error);
237
119
  }
120
+ } catch {
121
+ // 忽略环境变量检查错误
238
122
  }
239
-
240
- return result;
123
+ return createElementWithPropsAndChildren(tag, props, children);
241
124
  }
242
125
 
243
126
  /**
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { h, type JSXChildren } from "./jsx-factory";
11
11
  import { BaseComponent, type BaseComponentConfig } from "./base-component";
12
+ import { RenderContext } from "./render-context";
12
13
  import { createLogger } from "@wsxjs/wsx-logger";
13
14
 
14
15
  const logger = createLogger("LightComponent");
@@ -95,7 +96,7 @@ export abstract class LightComponent extends BaseComponent {
95
96
  childrenToRemove.forEach((child) => child.remove());
96
97
 
97
98
  // 渲染JSX内容到Light DOM
98
- const content = this.render();
99
+ const content = RenderContext.runInContext(this, () => this.render());
99
100
  this.appendChild(content);
100
101
 
101
102
  // 确保样式元素在第一个位置(如果存在)
@@ -178,7 +179,7 @@ export abstract class LightComponent extends BaseComponent {
178
179
 
179
180
  try {
180
181
  // 3. 重新渲染JSX内容
181
- const content = this.render();
182
+ const content = RenderContext.runInContext(this, () => this.render());
182
183
 
183
184
  // 4. 在添加到 DOM 之前恢复值,避免浏览器渲染状态值
184
185
  if (focusState && focusState.key && focusState.value !== undefined) {
@@ -78,6 +78,15 @@ export function state(
78
78
  // Compatibility with Babel plugin which is required for this decorator to work properly
79
79
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
80
80
  ): any {
81
+ // RFC 0037 Phase 0: Test Infrastructure Support
82
+ // Allow runtime decorator in tests to bypass Babel plugin requirement
83
+ // Use globalThis to safely access process in all environments
84
+
85
+ const globalProcess =
86
+ typeof globalThis !== "undefined" ? (globalThis as any).process : undefined;
87
+ if (globalProcess?.env?.NODE_ENV === "test") {
88
+ return;
89
+ }
81
90
  /**
82
91
  * @state decorator MUST be processed by Babel plugin at compile time.
83
92
  * If this function is executed at runtime, it means Babel plugin did not process it.
@@ -0,0 +1,40 @@
1
+ import { BaseComponent } from "./base-component";
2
+
3
+ /**
4
+ * RenderContext
5
+ *
6
+ * Tracks the currently rendering component instance.
7
+ * improved for RFC 0037 to support DOM caching and optimization.
8
+ */
9
+ export class RenderContext {
10
+ private static current: BaseComponent | null = null;
11
+
12
+ /**
13
+ * Executes a function within the context of a component.
14
+ * @param component The component instance currently rendering.
15
+ * @param fn The function to execute (usually the render method).
16
+ */
17
+ static runInContext<T>(component: BaseComponent, fn: () => T): T {
18
+ const prev = RenderContext.current;
19
+ RenderContext.current = component;
20
+ try {
21
+ return fn();
22
+ } finally {
23
+ RenderContext.current = prev;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Gets the currently rendering component.
29
+ */
30
+ static getCurrentComponent(): BaseComponent | null {
31
+ return RenderContext.current;
32
+ }
33
+
34
+ /**
35
+ * Gets the current component's DOM cache.
36
+ */
37
+ static getDOMCache() {
38
+ return RenderContext.current?.getDomCache();
39
+ }
40
+ }