@wsxjs/wsx-core 0.0.7 → 0.0.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wsxjs/wsx-core",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "Core WSX Framework - Web Components with JSX syntax",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Base Component for WSX Web Components
3
+ *
4
+ * Provides common functionality shared by WebComponent and LightComponent:
5
+ * - Reactive state management (reactive, useState, scheduleRerender)
6
+ * - Configuration management (getConfig, setConfig)
7
+ * - Attribute helpers (getAttr, setAttr, removeAttr, hasAttr)
8
+ * - Lifecycle hooks (onConnected, onDisconnected, onAttributeChanged)
9
+ */
10
+
11
+ import { reactive as createReactive, createState, reactiveWithDebug } from "./utils/reactive";
12
+
13
+ /**
14
+ * Type for reactive state storage
15
+ */
16
+ interface ReactiveStateStorage {
17
+ getter: () => unknown;
18
+ setter: (value: unknown | ((prev: unknown) => unknown)) => void;
19
+ }
20
+
21
+ /**
22
+ * Base configuration interface
23
+ */
24
+ export interface BaseComponentConfig {
25
+ styles?: string;
26
+ autoStyles?: string;
27
+ styleName?: string;
28
+ debug?: boolean;
29
+ [key: string]: unknown;
30
+ }
31
+
32
+ /**
33
+ * Base Component class with common functionality
34
+ *
35
+ * This class provides shared functionality for both WebComponent and LightComponent.
36
+ * It should not be used directly - use WebComponent or LightComponent instead.
37
+ */
38
+ export abstract class BaseComponent extends HTMLElement {
39
+ protected config: BaseComponentConfig;
40
+ protected connected: boolean = false;
41
+ protected _isDebugEnabled: boolean = false;
42
+ protected _reactiveStates = new Map<string, ReactiveStateStorage>();
43
+
44
+ /**
45
+ * Auto-injected styles from Babel plugin (if CSS file exists)
46
+ * @internal - Managed by babel-plugin-wsx-style
47
+ */
48
+ protected _autoStyles?: string;
49
+
50
+ /**
51
+ * 子类应该重写这个方法来定义观察的属性
52
+ * @returns 要观察的属性名数组
53
+ */
54
+ static get observedAttributes(): string[] {
55
+ return [];
56
+ }
57
+
58
+ constructor(config: BaseComponentConfig = {}) {
59
+ super();
60
+
61
+ this._isDebugEnabled = config.debug ?? false;
62
+
63
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
64
+ const host = this;
65
+
66
+ // Store original styles value to avoid infinite recursion in getter
67
+ const originalStyles = config.styles;
68
+
69
+ this.config = {
70
+ ...config,
71
+ get styles() {
72
+ // Auto-detect injected styles from class property
73
+ // Note: _defineProperty executes in constructor after super(),
74
+ // so we check _autoStyles dynamically via getter
75
+ // This works for both WebComponent and LightComponent
76
+ // Priority: originalStyles > _autoStyles
77
+ const result = originalStyles || host._autoStyles || "";
78
+ return result;
79
+ },
80
+ set styles(value: string) {
81
+ config.styles = value;
82
+ },
83
+ };
84
+ }
85
+
86
+ /**
87
+ * 抽象方法:子类必须实现JSX渲染
88
+ *
89
+ * @returns JSX元素
90
+ */
91
+ abstract render(): HTMLElement;
92
+
93
+ /**
94
+ * 可选生命周期钩子:组件已连接
95
+ */
96
+ protected onConnected?(): void;
97
+
98
+ /**
99
+ * 可选生命周期钩子:组件已断开
100
+ */
101
+ protected onDisconnected?(): void;
102
+
103
+ /**
104
+ * 可选生命周期钩子:属性已更改
105
+ */
106
+ protected onAttributeChanged?(name: string, oldValue: string, newValue: string): void;
107
+
108
+ /**
109
+ * Web Component生命周期:属性变化
110
+ */
111
+ attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
112
+ this.onAttributeChanged?.(name, oldValue, newValue);
113
+ }
114
+
115
+ /**
116
+ * 创建响应式对象
117
+ *
118
+ * @param obj 要变为响应式的对象
119
+ * @param debugName 调试名称(可选)
120
+ * @returns 响应式代理对象
121
+ */
122
+ protected reactive<T extends object>(obj: T, debugName?: string): T {
123
+ const reactiveFn = this._isDebugEnabled ? reactiveWithDebug : createReactive;
124
+ const name = debugName || `${this.constructor.name}.reactive`;
125
+
126
+ return this._isDebugEnabled
127
+ ? reactiveFn(obj, () => this.scheduleRerender(), name)
128
+ : reactiveFn(obj, () => this.scheduleRerender());
129
+ }
130
+
131
+ /**
132
+ * 创建响应式状态
133
+ *
134
+ * @param key 状态标识符
135
+ * @param initialValue 初始值
136
+ * @returns [getter, setter] 元组
137
+ */
138
+ protected useState<T>(
139
+ key: string,
140
+ initialValue: T
141
+ ): [() => T, (value: T | ((prev: T) => T)) => void] {
142
+ if (!this._reactiveStates.has(key)) {
143
+ const [getter, setter] = createState(initialValue, () => this.scheduleRerender());
144
+ this._reactiveStates.set(key, {
145
+ getter: getter as () => unknown,
146
+ setter: setter as (value: unknown | ((prev: unknown) => unknown)) => void,
147
+ });
148
+ }
149
+
150
+ const state = this._reactiveStates.get(key);
151
+ if (!state) {
152
+ throw new Error(`State ${key} not found`);
153
+ }
154
+ return [state.getter as () => T, state.setter as (value: T | ((prev: T) => T)) => void];
155
+ }
156
+
157
+ /**
158
+ * 调度重渲染
159
+ * 这个方法被响应式系统调用,开发者通常不需要直接调用
160
+ */
161
+ protected scheduleRerender(): void {
162
+ if (this.connected) {
163
+ this.rerender();
164
+ }
165
+ }
166
+
167
+ /**
168
+ * 重新渲染组件(子类需要实现)
169
+ */
170
+ protected abstract rerender(): void;
171
+
172
+ /**
173
+ * 获取配置值
174
+ *
175
+ * @param key - 配置键
176
+ * @param defaultValue - 默认值
177
+ * @returns 配置值
178
+ */
179
+ protected getConfig<T>(key: string, defaultValue?: T): T {
180
+ return (this.config[key] as T) ?? (defaultValue as T);
181
+ }
182
+
183
+ /**
184
+ * 设置配置值
185
+ *
186
+ * @param key - 配置键
187
+ * @param value - 配置值
188
+ */
189
+ protected setConfig(key: string, value: unknown): void {
190
+ this.config[key] = value;
191
+ }
192
+
193
+ /**
194
+ * 获取属性值
195
+ *
196
+ * @param name - 属性名
197
+ * @param defaultValue - 默认值
198
+ * @returns 属性值
199
+ */
200
+ protected getAttr(name: string, defaultValue = ""): string {
201
+ return this.getAttribute(name) || defaultValue;
202
+ }
203
+
204
+ /**
205
+ * 设置属性值
206
+ *
207
+ * @param name - 属性名
208
+ * @param value - 属性值
209
+ */
210
+ protected setAttr(name: string, value: string): void {
211
+ this.setAttribute(name, value);
212
+ }
213
+
214
+ /**
215
+ * 移除属性
216
+ *
217
+ * @param name - 属性名
218
+ */
219
+ protected removeAttr(name: string): void {
220
+ this.removeAttribute(name);
221
+ }
222
+
223
+ /**
224
+ * 检查是否有属性
225
+ *
226
+ * @param name - 属性名
227
+ * @returns 是否存在
228
+ */
229
+ protected hasAttr(name: string): boolean {
230
+ return this.hasAttribute(name);
231
+ }
232
+
233
+ /**
234
+ * 清理响应式状态
235
+ */
236
+ protected cleanupReactiveStates(): void {
237
+ this._reactiveStates.clear();
238
+ }
239
+ }
package/src/index.ts CHANGED
@@ -6,9 +6,8 @@ export { h, h as jsx, h as jsxs, Fragment } from "./jsx-factory";
6
6
  export { StyleManager } from "./styles/style-manager";
7
7
  export { WSXLogger, logger, createLogger } from "./utils/logger";
8
8
 
9
- // Reactive exports
10
- export { reactive, createState, ReactiveDebug } from "./utils/reactive";
11
- export { ReactiveWebComponent, makeReactive, createReactiveComponent } from "./reactive-component";
9
+ // Reactive exports - Decorator-based API
10
+ export { state } from "./reactive-decorator";
12
11
 
13
12
  // Type exports
14
13
  export type { WebComponentConfig } from "./web-component";
@@ -16,4 +15,3 @@ export type { LightComponentConfig } from "./light-component";
16
15
  export type { JSXChildren } from "./jsx-factory";
17
16
  export type { Logger, LogLevel } from "./utils/logger";
18
17
  export type { ReactiveCallback } from "./utils/reactive";
19
- export type { ReactiveWebComponentConfig } from "./reactive-component";
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { h, type JSXChildren } from "./jsx-factory";
11
- import { reactive, createState, reactiveWithDebug } from "./utils/reactive";
11
+ import { BaseComponent, type BaseComponentConfig } from "./base-component";
12
12
  import { createLogger } from "./utils/logger";
13
13
 
14
14
  const logger = createLogger("LightComponent");
@@ -16,35 +16,17 @@ const logger = createLogger("LightComponent");
16
16
  /**
17
17
  * Light DOM Component 配置接口
18
18
  */
19
- export interface LightComponentConfig {
20
- styles?: string; // CSS内容
21
- styleName?: string; // 样式名称,用于缓存
22
- debug?: boolean; // 是否启用响应式调试模式
23
- [key: string]: unknown;
24
- }
19
+ export type LightComponentConfig = BaseComponentConfig;
25
20
 
26
21
  /**
27
22
  * Light DOM WSX Web Component 基类
28
23
  */
29
- export abstract class LightComponent extends HTMLElement {
30
- protected config: LightComponentConfig;
31
- protected connected: boolean = false;
32
- private _isDebugEnabled: boolean = false;
33
- private _reactiveStates = new Map<string, any>();
34
-
35
- /**
36
- * 子类应该重写这个方法来定义观察的属性
37
- * @returns 要观察的属性名数组
38
- */
39
- static get observedAttributes(): string[] {
40
- return [];
41
- }
24
+ export abstract class LightComponent extends BaseComponent {
25
+ protected config!: LightComponentConfig; // Initialized by BaseComponent constructor
42
26
 
43
27
  constructor(config: LightComponentConfig = {}) {
44
- super();
45
-
46
- this.config = config;
47
- this._isDebugEnabled = config.debug ?? false;
28
+ super(config);
29
+ // BaseComponent already created this.config with getter for styles
48
30
  }
49
31
 
50
32
  /**
@@ -61,9 +43,14 @@ export abstract class LightComponent extends HTMLElement {
61
43
  this.connected = true;
62
44
  try {
63
45
  // 应用CSS样式到组件自身
64
- if (this.config.styles) {
46
+ // CRITICAL: _defineProperty for class properties executes AFTER super() but BEFORE constructor body
47
+ // However, in practice, _defineProperty may execute AFTER the constructor body
48
+ // So we need to check _autoStyles directly first, then fallback to config.styles getter
49
+ // The getter will dynamically check _autoStyles when accessed
50
+ const stylesToApply = this._autoStyles || this.config.styles;
51
+ if (stylesToApply) {
65
52
  const styleName = this.config.styleName || this.constructor.name;
66
- this.applyScopedStyles(styleName, this.config.styles);
53
+ this.applyScopedStyles(styleName, stylesToApply);
67
54
  }
68
55
 
69
56
  // 渲染JSX内容到Light DOM
@@ -87,28 +74,6 @@ export abstract class LightComponent extends HTMLElement {
87
74
  this.onDisconnected?.();
88
75
  }
89
76
 
90
- /**
91
- * Web Component生命周期:属性变化
92
- */
93
- attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
94
- this.onAttributeChanged?.(name, oldValue, newValue);
95
- }
96
-
97
- /**
98
- * 可选生命周期钩子:组件已连接
99
- */
100
- protected onConnected?(): void;
101
-
102
- /**
103
- * 可选生命周期钩子:组件已断开
104
- */
105
- protected onDisconnected?(): void;
106
-
107
- /**
108
- * 可选生命周期钩子:属性已更改
109
- */
110
- protected onAttributeChanged?(name: string, oldValue: string, newValue: string): void;
111
-
112
77
  /**
113
78
  * 查找组件内的元素
114
79
  *
@@ -129,53 +94,6 @@ export abstract class LightComponent extends HTMLElement {
129
94
  return HTMLElement.prototype.querySelectorAll.call(this, selector) as NodeListOf<T>;
130
95
  }
131
96
 
132
- /**
133
- * 创建响应式对象
134
- *
135
- * @param obj 要变为响应式的对象
136
- * @param debugName 调试名称(可选)
137
- * @returns 响应式代理对象
138
- */
139
- protected reactive<T extends object>(obj: T, debugName?: string): T {
140
- const reactiveFn = this._isDebugEnabled ? reactiveWithDebug : reactive;
141
- const name = debugName || `${this.constructor.name}.reactive`;
142
-
143
- return this._isDebugEnabled
144
- ? reactiveFn(obj, () => this.scheduleRerender(), name)
145
- : reactiveFn(obj, () => this.scheduleRerender());
146
- }
147
-
148
- /**
149
- * 创建响应式状态
150
- *
151
- * @param key 状态标识符
152
- * @param initialValue 初始值
153
- * @returns [getter, setter] 元组
154
- */
155
- protected useState<T>(
156
- key: string,
157
- initialValue: T
158
- ): [() => T, (value: T | ((prev: T) => T)) => void] {
159
- if (!this._reactiveStates.has(key)) {
160
- const [getter, setter] = createState(initialValue, () => this.scheduleRerender());
161
- this._reactiveStates.set(key, { getter, setter });
162
- }
163
-
164
- const state = this._reactiveStates.get(key);
165
- return [state.getter, state.setter];
166
- }
167
-
168
- /**
169
- * 调度重渲染
170
- * 这个方法被响应式系统调用,开发者通常不需要直接调用
171
- */
172
- protected scheduleRerender(): void {
173
- // 确保组件已连接到 DOM
174
- if (this.connected) {
175
- this.rerender();
176
- }
177
- }
178
-
179
97
  /**
180
98
  * 重新渲染组件
181
99
  */
@@ -266,34 +184,6 @@ export abstract class LightComponent extends HTMLElement {
266
184
  this.insertBefore(styleElement, this.firstChild);
267
185
  }
268
186
 
269
- /**
270
- * 获取配置值
271
- *
272
- * @param key - 配置键
273
- * @param defaultValue - 默认值
274
- * @returns 配置值
275
- */
276
- protected getConfig<T>(key: string, defaultValue?: T): T {
277
- return (this.config[key] as T) ?? (defaultValue as T);
278
- }
279
-
280
- /**
281
- * 设置配置值
282
- *
283
- * @param key - 配置键
284
- * @param value - 配置值
285
- */
286
- protected setConfig(key: string, value: unknown): void {
287
- this.config[key] = value;
288
- }
289
-
290
- /**
291
- * 清理响应式状态
292
- */
293
- private cleanupReactiveStates(): void {
294
- this._reactiveStates.clear();
295
- }
296
-
297
187
  /**
298
188
  * 清理组件样式
299
189
  */
@@ -304,46 +194,6 @@ export abstract class LightComponent extends HTMLElement {
304
194
  existingStyle.remove();
305
195
  }
306
196
  }
307
-
308
- /**
309
- * 获取属性值
310
- *
311
- * @param name - 属性名
312
- * @param defaultValue - 默认值
313
- * @returns 属性值
314
- */
315
- protected getAttr(name: string, defaultValue = ""): string {
316
- return this.getAttribute(name) || defaultValue;
317
- }
318
-
319
- /**
320
- * 设置属性值
321
- *
322
- * @param name - 属性名
323
- * @param value - 属性值
324
- */
325
- protected setAttr(name: string, value: string): void {
326
- this.setAttribute(name, value);
327
- }
328
-
329
- /**
330
- * 移除属性
331
- *
332
- * @param name - 属性名
333
- */
334
- protected removeAttr(name: string): void {
335
- this.removeAttribute(name);
336
- }
337
-
338
- /**
339
- * 检查是否有属性
340
- *
341
- * @param name - 属性名
342
- * @returns 是否存在
343
- */
344
- protected hasAttr(name: string): boolean {
345
- return this.hasAttribute(name);
346
- }
347
197
  }
348
198
 
349
199
  // 导出JSX助手
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Reactive Decorators for WSX Components
3
+ *
4
+ * Provides @state property decorator to mark properties as reactive state.
5
+ * WebComponent and LightComponent already have reactive() and useState() methods.
6
+ * Babel plugin handles @state initialization at compile time.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * class Counter extends WebComponent {
11
+ * @state private count = 0;
12
+ * @state private user = { name: "John" };
13
+ * }
14
+ * ```
15
+ */
16
+
17
+ /**
18
+ * State property decorator
19
+ *
20
+ * Marks a property as reactive state. Babel plugin processes this decorator at compile time
21
+ * and generates initialization code in the constructor.
22
+ *
23
+ * Automatically uses reactive() for objects/arrays and useState() for primitives.
24
+ *
25
+ * @param target - The class prototype
26
+ * @param propertyKey - The property name
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * class MyComponent extends WebComponent {
31
+ * @state private count = 0; // Auto-initialized by Babel plugin
32
+ * @state private user = { name: "John" }; // Auto-initialized by Babel plugin
33
+ * }
34
+ * ```
35
+ */
36
+ export function state(target: unknown, propertyKey: string | symbol | unknown): void {
37
+ /**
38
+ * @state decorator is a compile-time marker for Babel plugin.
39
+ * Babel plugin will:
40
+ * 1. Detect @state decorator on properties
41
+ * 2. Extract initial value from AST
42
+ * 3. Remove @state decorator
43
+ * 4. Generate initialization code in constructor (this.state = this.reactive(...) or useState)
44
+ *
45
+ * This runtime function only performs basic validation.
46
+ * If Babel plugin is not configured, this will throw an error.
47
+ */
48
+
49
+ // Normalize propertyKey
50
+ let normalizedPropertyKey: string | symbol;
51
+ if (typeof propertyKey === "string" || typeof propertyKey === "symbol") {
52
+ normalizedPropertyKey = propertyKey;
53
+ } else {
54
+ const propertyKeyStr = String(propertyKey);
55
+ if (propertyKeyStr === "[object Object]") {
56
+ // Invalid propertyKey - likely a build configuration issue
57
+ throw new Error(
58
+ `@state decorator: Invalid propertyKey. ` +
59
+ `This usually means the build tool doesn't support decorators properly. ` +
60
+ `Please ensure Babel plugin is configured in vite.config.ts`
61
+ );
62
+ }
63
+ normalizedPropertyKey = propertyKeyStr;
64
+ }
65
+
66
+ // Basic validation: ensure target is valid
67
+ if (target == null) {
68
+ const propertyKeyStr =
69
+ typeof normalizedPropertyKey === "string"
70
+ ? normalizedPropertyKey
71
+ : normalizedPropertyKey.toString();
72
+ throw new Error(
73
+ `@state decorator: Cannot access property "${propertyKeyStr}". ` +
74
+ `Target is ${target === null ? "null" : "undefined"}. ` +
75
+ `Please ensure Babel plugin is configured in vite.config.ts`
76
+ );
77
+ }
78
+
79
+ if (typeof target !== "object") {
80
+ const propertyKeyStr =
81
+ typeof normalizedPropertyKey === "string"
82
+ ? normalizedPropertyKey
83
+ : normalizedPropertyKey.toString();
84
+ throw new Error(
85
+ `@state decorator: Cannot be used on "${propertyKeyStr}". ` +
86
+ `@state is for properties only, not methods.`
87
+ );
88
+ }
89
+
90
+ // Validate that property has an initial value
91
+ const descriptor = Object.getOwnPropertyDescriptor(target, normalizedPropertyKey);
92
+ if (descriptor?.get) {
93
+ const propertyKeyStr =
94
+ typeof normalizedPropertyKey === "string"
95
+ ? normalizedPropertyKey
96
+ : normalizedPropertyKey.toString();
97
+ throw new Error(
98
+ `@state decorator cannot be used with getter properties. Property: "${propertyKeyStr}"`
99
+ );
100
+ }
101
+
102
+ // Note: We don't store metadata or remove the property here.
103
+ // Babel plugin handles everything at compile time.
104
+ // If this function is called at runtime, it means Babel plugin didn't process the decorator.
105
+ }