@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/dist/index.js +297 -424
- package/dist/index.mjs +295 -417
- package/package.json +1 -1
- package/src/base-component.ts +239 -0
- package/src/index.ts +2 -4
- package/src/light-component.ts +13 -163
- package/src/reactive-decorator.ts +105 -0
- package/src/web-component.ts +157 -110
- package/types/index.d.ts +2 -2
- package/src/reactive-component.ts +0 -306
package/package.json
CHANGED
|
@@ -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 {
|
|
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";
|
package/src/light-component.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { h, type JSXChildren } from "./jsx-factory";
|
|
11
|
-
import {
|
|
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
|
|
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
|
|
30
|
-
protected config
|
|
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
|
-
|
|
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,
|
|
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
|
+
}
|