@wsxjs/wsx-core 0.0.8 → 0.0.10

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.
@@ -167,6 +167,13 @@ function h(tag, props = {}, ...children) {
167
167
  if (value) {
168
168
  element.setAttribute(key, "");
169
169
  }
170
+ } else if (key === "value") {
171
+ if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) {
172
+ element.value = String(value);
173
+ } else {
174
+ const attributeName = isSVG ? getSVGAttributeName(key) : key;
175
+ element.setAttribute(attributeName, String(value));
176
+ }
170
177
  } else {
171
178
  const attributeName = isSVG ? getSVGAttributeName(key) : key;
172
179
  element.setAttribute(attributeName, String(value));
@@ -2,7 +2,7 @@ import {
2
2
  Fragment,
3
3
  jsx,
4
4
  jsxs
5
- } from "./chunk-VZQT7HU5.mjs";
5
+ } from "./chunk-CZII6RG2.mjs";
6
6
  export {
7
7
  Fragment,
8
8
  jsx,
package/dist/jsx.js CHANGED
@@ -166,6 +166,13 @@ function h(tag, props = {}, ...children) {
166
166
  if (value) {
167
167
  element.setAttribute(key, "");
168
168
  }
169
+ } else if (key === "value") {
170
+ if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) {
171
+ element.value = String(value);
172
+ } else {
173
+ const attributeName = isSVG ? getSVGAttributeName(key) : key;
174
+ element.setAttribute(attributeName, String(value));
175
+ }
169
176
  } else {
170
177
  const attributeName = isSVG ? getSVGAttributeName(key) : key;
171
178
  element.setAttribute(attributeName, String(value));
package/dist/jsx.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  Fragment,
3
3
  h
4
- } from "./chunk-VZQT7HU5.mjs";
4
+ } from "./chunk-CZII6RG2.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.8",
3
+ "version": "0.0.10",
4
4
  "description": "Core WSX Framework - Web Components with JSX syntax",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -18,6 +18,19 @@ interface ReactiveStateStorage {
18
18
  setter: (value: unknown | ((prev: unknown) => unknown)) => void;
19
19
  }
20
20
 
21
+ /**
22
+ * Focus state interface for focus preservation during rerender
23
+ */
24
+ interface FocusState {
25
+ key: string;
26
+ elementType: "input" | "textarea" | "select" | "contenteditable";
27
+ value?: string;
28
+ selectionStart?: number;
29
+ selectionEnd?: number;
30
+ scrollTop?: number; // For textarea
31
+ selectedIndex?: number; // For select
32
+ }
33
+
21
34
  /**
22
35
  * Base configuration interface
23
36
  */
@@ -47,6 +60,24 @@ export abstract class BaseComponent extends HTMLElement {
47
60
  */
48
61
  protected _autoStyles?: string;
49
62
 
63
+ /**
64
+ * 当前捕获的焦点状态(用于在 render 时使用捕获的值)
65
+ * @internal - 由 rerender() 方法管理
66
+ */
67
+ protected _pendingFocusState: FocusState | null = null;
68
+
69
+ /**
70
+ * 防抖定时器,用于延迟重渲染(当用户正在输入时)
71
+ * @internal
72
+ */
73
+ private _rerenderDebounceTimer: number | null = null;
74
+
75
+ /**
76
+ * 待处理的重渲染标志(当用户正在输入时,标记需要重渲染但延迟执行)
77
+ * @internal
78
+ */
79
+ private _pendingRerender: boolean = false;
80
+
50
81
  /**
51
82
  * 子类应该重写这个方法来定义观察的属性
52
83
  * @returns 要观察的属性名数组
@@ -95,6 +126,35 @@ export abstract class BaseComponent extends HTMLElement {
95
126
  */
96
127
  protected onConnected?(): void;
97
128
 
129
+ /**
130
+ * 处理 blur 事件,在用户停止输入时执行待处理的重渲染
131
+ * @internal
132
+ */
133
+ private handleGlobalBlur = (event: FocusEvent): void => {
134
+ // 检查 blur 的元素是否在组件内
135
+ const root = this.getActiveRoot();
136
+ const target = event.target as HTMLElement;
137
+
138
+ if (target && root.contains(target)) {
139
+ // 用户停止输入,执行待处理的重渲染
140
+ if (this._pendingRerender && this.connected) {
141
+ // 清除防抖定时器
142
+ if (this._rerenderDebounceTimer !== null) {
143
+ clearTimeout(this._rerenderDebounceTimer);
144
+ this._rerenderDebounceTimer = null;
145
+ }
146
+
147
+ // 延迟一小段时间后重渲染,确保 blur 事件完全处理
148
+ requestAnimationFrame(() => {
149
+ if (this._pendingRerender && this.connected) {
150
+ this._pendingRerender = false;
151
+ this.rerender();
152
+ }
153
+ });
154
+ }
155
+ }
156
+ };
157
+
98
158
  /**
99
159
  * 可选生命周期钩子:组件已断开
100
160
  */
@@ -157,11 +217,99 @@ export abstract class BaseComponent extends HTMLElement {
157
217
  /**
158
218
  * 调度重渲染
159
219
  * 这个方法被响应式系统调用,开发者通常不需要直接调用
220
+ * 使用 queueMicrotask 进行异步调度,与 reactive() 系统保持一致
160
221
  */
161
222
  protected scheduleRerender(): void {
162
- if (this.connected) {
163
- this.rerender();
223
+ if (!this.connected) {
224
+ // 如果组件已断开,清除定时器
225
+ if (this._rerenderDebounceTimer !== null) {
226
+ clearTimeout(this._rerenderDebounceTimer);
227
+ this._rerenderDebounceTimer = null;
228
+ }
229
+ return;
164
230
  }
231
+
232
+ // 检查是否有需要持续输入的元素获得焦点(input、textarea、select、contenteditable)
233
+ // 按钮等其他元素应该立即重渲染,以反映状态变化
234
+ const root = this.getActiveRoot();
235
+ let activeElement: HTMLElement | null = null;
236
+
237
+ if (root instanceof ShadowRoot) {
238
+ activeElement = root.activeElement as HTMLElement | null;
239
+ } else {
240
+ const docActiveElement = document.activeElement;
241
+ if (docActiveElement && root.contains(docActiveElement)) {
242
+ activeElement = docActiveElement as HTMLElement;
243
+ }
244
+ }
245
+
246
+ // 只对需要持续输入的元素跳过重渲染(input、textarea、select、contenteditable)
247
+ // 按钮等其他元素应该立即重渲染
248
+ // 如果元素有 data-wsx-force-render 属性,即使是要持续输入的元素也强制重渲染
249
+ if (activeElement) {
250
+ const isInputElement =
251
+ activeElement instanceof HTMLInputElement ||
252
+ activeElement instanceof HTMLTextAreaElement ||
253
+ activeElement instanceof HTMLSelectElement ||
254
+ activeElement.hasAttribute("contenteditable");
255
+
256
+ // 检查是否有强制渲染属性
257
+ const forceRender = activeElement.hasAttribute("data-wsx-force-render");
258
+
259
+ // 如果是输入元素且没有强制渲染属性,跳过重渲染,等待 blur 事件
260
+ if (isInputElement && !forceRender) {
261
+ // 标记需要重渲染,但延迟到 blur 事件
262
+ this._pendingRerender = true;
263
+
264
+ // 清除之前的定时器(不再使用定时器,只等待 blur)
265
+ if (this._rerenderDebounceTimer !== null) {
266
+ clearTimeout(this._rerenderDebounceTimer);
267
+ this._rerenderDebounceTimer = null;
268
+ }
269
+
270
+ // 不执行重渲染,等待 blur 事件
271
+ return;
272
+ }
273
+ // 对于按钮等其他元素,或者有 data-wsx-force-render 属性的输入元素,继续执行重渲染(不跳过)
274
+ }
275
+
276
+ // 没有焦点元素,立即重渲染(使用 queueMicrotask 批量处理)
277
+ // 如果有待处理的重渲染,也立即执行
278
+ if (this._pendingRerender) {
279
+ this._pendingRerender = false;
280
+ }
281
+ queueMicrotask(() => {
282
+ if (this.connected) {
283
+ this.rerender();
284
+ }
285
+ });
286
+ }
287
+
288
+ /**
289
+ * 清理资源(在组件断开连接时调用)
290
+ * @internal
291
+ */
292
+ protected cleanup(): void {
293
+ // 清除防抖定时器
294
+ if (this._rerenderDebounceTimer !== null) {
295
+ clearTimeout(this._rerenderDebounceTimer);
296
+ this._rerenderDebounceTimer = null;
297
+ }
298
+
299
+ // 移除 blur 事件监听器
300
+ document.removeEventListener("blur", this.handleGlobalBlur, true);
301
+
302
+ // 清除待处理的重渲染标志
303
+ this._pendingRerender = false;
304
+ }
305
+
306
+ /**
307
+ * 初始化事件监听器(在组件连接时调用)
308
+ * @internal
309
+ */
310
+ protected initializeEventListeners(): void {
311
+ // 添加 blur 事件监听器,在用户停止输入时执行待处理的重渲染
312
+ document.addEventListener("blur", this.handleGlobalBlur, true);
165
313
  }
166
314
 
167
315
  /**
@@ -236,4 +384,183 @@ export abstract class BaseComponent extends HTMLElement {
236
384
  protected cleanupReactiveStates(): void {
237
385
  this._reactiveStates.clear();
238
386
  }
387
+
388
+ /**
389
+ * 获取当前活动的 DOM 根(Shadow DOM 或 Light DOM)
390
+ * @returns 活动的 DOM 根元素
391
+ */
392
+ protected getActiveRoot(): ShadowRoot | HTMLElement {
393
+ // WebComponent 使用 shadowRoot,LightComponent 使用自身
394
+ if ("shadowRoot" in this && this.shadowRoot) {
395
+ return this.shadowRoot;
396
+ }
397
+ return this;
398
+ }
399
+
400
+ /**
401
+ * 捕获当前焦点状态(在重渲染之前调用)
402
+ * @returns 焦点状态,如果没有焦点元素则返回 null
403
+ */
404
+ protected captureFocusState(): FocusState | null {
405
+ const root = this.getActiveRoot();
406
+ let activeElement: Element | null = null;
407
+
408
+ // 获取活动元素
409
+ if (root instanceof ShadowRoot) {
410
+ // Shadow DOM: 使用 shadowRoot.activeElement
411
+ activeElement = root.activeElement;
412
+ } else {
413
+ // Light DOM: 检查 document.activeElement 是否在组件内
414
+ const docActiveElement = document.activeElement;
415
+ if (docActiveElement && root.contains(docActiveElement)) {
416
+ activeElement = docActiveElement;
417
+ }
418
+ }
419
+
420
+ if (!activeElement || !(activeElement instanceof HTMLElement)) {
421
+ return null;
422
+ }
423
+
424
+ // 检查元素是否有 data-wsx-key 属性
425
+ const key = activeElement.getAttribute("data-wsx-key");
426
+ if (!key) {
427
+ return null; // 元素没有 key,跳过焦点保持
428
+ }
429
+
430
+ const tagName = activeElement.tagName.toLowerCase();
431
+ const state: FocusState = {
432
+ key,
433
+ elementType: tagName as FocusState["elementType"],
434
+ };
435
+
436
+ // 处理 input 和 textarea
437
+ if (
438
+ activeElement instanceof HTMLInputElement ||
439
+ activeElement instanceof HTMLTextAreaElement
440
+ ) {
441
+ state.value = activeElement.value;
442
+ state.selectionStart = activeElement.selectionStart ?? undefined;
443
+ state.selectionEnd = activeElement.selectionEnd ?? undefined;
444
+
445
+ // 对于 textarea,保存滚动位置
446
+ if (activeElement instanceof HTMLTextAreaElement) {
447
+ state.scrollTop = activeElement.scrollTop;
448
+ }
449
+ }
450
+ // 处理 select
451
+ else if (activeElement instanceof HTMLSelectElement) {
452
+ state.elementType = "select";
453
+ state.selectedIndex = activeElement.selectedIndex;
454
+ }
455
+ // 处理 contenteditable
456
+ else if (activeElement.hasAttribute("contenteditable")) {
457
+ state.elementType = "contenteditable";
458
+ const selection = window.getSelection();
459
+ if (selection && selection.rangeCount > 0) {
460
+ const range = selection.getRangeAt(0);
461
+ state.selectionStart = range.startOffset;
462
+ state.selectionEnd = range.endOffset;
463
+ }
464
+ }
465
+
466
+ return state;
467
+ }
468
+
469
+ /**
470
+ * 恢复焦点状态(在重渲染之后调用)
471
+ * @param state - 之前捕获的焦点状态
472
+ */
473
+ protected restoreFocusState(state: FocusState | null): void {
474
+ if (!state || !state.key) {
475
+ return;
476
+ }
477
+
478
+ const root = this.getActiveRoot();
479
+ const target = root.querySelector(`[data-wsx-key="${state.key}"]`) as HTMLElement;
480
+
481
+ if (!target) {
482
+ return; // 元素未找到,跳过恢复
483
+ }
484
+
485
+ // 立即同步恢复值,避免闪烁
486
+ // 这必须在 appendChild 之后立即执行,在浏览器渲染之前
487
+ if (state.value !== undefined) {
488
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
489
+ // 直接设置 value 属性,覆盖 render() 中设置的值
490
+ // 使用 .value 而不是 setAttribute,因为 .value 是当前值,setAttribute 是初始值
491
+ target.value = state.value;
492
+ }
493
+ }
494
+
495
+ // 恢复 select 状态
496
+ if (state.selectedIndex !== undefined && target instanceof HTMLSelectElement) {
497
+ target.selectedIndex = state.selectedIndex;
498
+ }
499
+
500
+ // 使用 requestAnimationFrame 恢复焦点和光标位置
501
+ // 这样可以确保 DOM 完全更新,但值已经同步恢复了
502
+ requestAnimationFrame(() => {
503
+ // 再次查找元素(确保元素仍然存在)
504
+ const currentTarget = root.querySelector(
505
+ `[data-wsx-key="${state.key}"]`
506
+ ) as HTMLElement;
507
+
508
+ if (!currentTarget) {
509
+ return;
510
+ }
511
+
512
+ // 再次确保值正确(防止被其他代码覆盖)
513
+ if (state.value !== undefined) {
514
+ if (
515
+ currentTarget instanceof HTMLInputElement ||
516
+ currentTarget instanceof HTMLTextAreaElement
517
+ ) {
518
+ // 只有在值不同时才更新,避免触发额外的事件
519
+ if (currentTarget.value !== state.value) {
520
+ currentTarget.value = state.value;
521
+ }
522
+ }
523
+ }
524
+
525
+ // 聚焦元素(防止页面滚动)
526
+ currentTarget.focus({ preventScroll: true });
527
+
528
+ // 恢复光标/选择位置
529
+ if (state.selectionStart !== undefined) {
530
+ if (
531
+ currentTarget instanceof HTMLInputElement ||
532
+ currentTarget instanceof HTMLTextAreaElement
533
+ ) {
534
+ const start = state.selectionStart;
535
+ const end = state.selectionEnd ?? start;
536
+ currentTarget.setSelectionRange(start, end);
537
+
538
+ // 恢复 textarea 滚动位置
539
+ if (
540
+ state.scrollTop !== undefined &&
541
+ currentTarget instanceof HTMLTextAreaElement
542
+ ) {
543
+ currentTarget.scrollTop = state.scrollTop;
544
+ }
545
+ } else if (currentTarget.hasAttribute("contenteditable")) {
546
+ // 恢复 contenteditable 选择
547
+ const selection = window.getSelection();
548
+ if (selection) {
549
+ const range = document.createRange();
550
+ const textNode = currentTarget.childNodes[0];
551
+ if (textNode && textNode.nodeType === Node.TEXT_NODE) {
552
+ const maxPos = Math.min(
553
+ state.selectionStart,
554
+ textNode.textContent?.length || 0
555
+ );
556
+ range.setStart(textNode, maxPos);
557
+ range.setEnd(textNode, state.selectionEnd ?? maxPos);
558
+ selection.removeAllRanges();
559
+ selection.addRange(range);
560
+ }
561
+ }
562
+ }
563
+ }
564
+ });
565
+ }
239
566
  }
@@ -89,6 +89,21 @@ export function h(
89
89
  element.setAttribute(key, "");
90
90
  }
91
91
  }
92
+ // 特殊处理 input/textarea/select 的 value 属性
93
+ // 使用 .value 而不是 setAttribute,因为 .value 是当前值,setAttribute 是初始值
94
+ else if (key === "value") {
95
+ if (
96
+ element instanceof HTMLInputElement ||
97
+ element instanceof HTMLTextAreaElement ||
98
+ element instanceof HTMLSelectElement
99
+ ) {
100
+ element.value = String(value);
101
+ } else {
102
+ // 对于其他元素,使用 setAttribute
103
+ const attributeName = isSVG ? getSVGAttributeName(key) : key;
104
+ element.setAttribute(attributeName, String(value));
105
+ }
106
+ }
92
107
  // 处理其他属性
93
108
  else {
94
109
  // 对SVG元素使用正确的属性名
@@ -57,6 +57,9 @@ export abstract class LightComponent extends BaseComponent {
57
57
  const content = this.render();
58
58
  this.appendChild(content);
59
59
 
60
+ // 初始化事件监听器
61
+ this.initializeEventListeners();
62
+
60
63
  // 调用子类的初始化钩子
61
64
  this.onConnected?.();
62
65
  } catch (error) {
@@ -69,6 +72,8 @@ export abstract class LightComponent extends BaseComponent {
69
72
  * Web Component生命周期:从DOM断开
70
73
  */
71
74
  disconnectedCallback(): void {
75
+ this.connected = false;
76
+ this.cleanup(); // 清理资源(包括防抖定时器)
72
77
  this.cleanupReactiveStates();
73
78
  this.cleanupStyles();
74
79
  this.onDisconnected?.();
@@ -105,36 +110,97 @@ export abstract class LightComponent extends BaseComponent {
105
110
  return;
106
111
  }
107
112
 
108
- // 清空现有内容(包括样式元素)
109
- this.innerHTML = "";
113
+ // 1. 捕获焦点状态(在 DOM 替换之前)
114
+ const focusState = this.captureFocusState();
115
+ // 保存到实例变量,供 render() 使用(如果需要)
116
+ this._pendingFocusState = focusState;
110
117
 
111
- // 重新应用样式(必须在内容之前添加,确保样式优先)
112
- if (this.config.styles) {
113
- const styleName = this.config.styleName || this.constructor.name;
114
- // 直接创建并添加样式元素,不检查是否存在(因为 innerHTML = "" 已经清空了)
115
- const styleElement = document.createElement("style");
116
- styleElement.setAttribute("data-wsx-light-component", styleName);
117
- styleElement.textContent = this.config.styles;
118
- // 使用 prepend 或 insertBefore 确保样式在第一个位置
119
- // 由于 innerHTML = "" 后 firstChild 是 null,使用 appendChild 然后调整顺序
120
- this.appendChild(styleElement);
121
- }
122
-
123
- // 重新渲染JSX内容
124
118
  try {
119
+ // 重新渲染JSX内容
125
120
  const content = this.render();
126
- this.appendChild(content);
127
121
 
128
- // 确保样式元素在内容之前(如果样式存在)
129
- if (this.config.styles && this.children.length > 1) {
130
- const styleElement = this.querySelector(
131
- `style[data-wsx-light-component="${this.config.styleName || this.constructor.name}"]`
132
- );
133
- if (styleElement && styleElement !== this.firstChild) {
134
- // 将样式元素移到第一个位置
122
+ // 在添加到 DOM 之前恢复值,避免浏览器渲染状态值
123
+ // 这样可以确保值在元素添加到 DOM 之前就是正确的
124
+ if (focusState && focusState.key && focusState.value !== undefined) {
125
+ // content 树中查找目标元素
126
+ const target = content.querySelector(
127
+ `[data-wsx-key="${focusState.key}"]`
128
+ ) as HTMLElement;
129
+
130
+ if (target) {
131
+ if (
132
+ target instanceof HTMLInputElement ||
133
+ target instanceof HTMLTextAreaElement
134
+ ) {
135
+ target.value = focusState.value;
136
+ }
137
+ }
138
+ }
139
+
140
+ // 确保样式元素存在
141
+ const stylesToApply = this._autoStyles || this.config.styles;
142
+ if (stylesToApply) {
143
+ const styleName = this.config.styleName || this.constructor.name;
144
+ let styleElement = this.querySelector(
145
+ `style[data-wsx-light-component="${styleName}"]`
146
+ ) as HTMLStyleElement;
147
+
148
+ if (!styleElement) {
149
+ // 创建样式元素
150
+ styleElement = document.createElement("style");
151
+ styleElement.setAttribute("data-wsx-light-component", styleName);
152
+ styleElement.textContent = stylesToApply;
135
153
  this.insertBefore(styleElement, this.firstChild);
154
+ } else if (styleElement.textContent !== stylesToApply) {
155
+ // 更新样式内容
156
+ styleElement.textContent = stylesToApply;
136
157
  }
137
158
  }
159
+
160
+ // 使用 requestAnimationFrame 批量执行 DOM 操作,减少重绘
161
+ // 在同一帧中完成添加和移除,避免中间状态
162
+ requestAnimationFrame(() => {
163
+ // 先添加新内容
164
+ this.appendChild(content);
165
+
166
+ // 立即移除旧内容(在同一帧中,浏览器会批量处理)
167
+ const oldChildren = Array.from(this.children).filter((child) => {
168
+ // 保留新添加的内容
169
+ if (child === content) {
170
+ return false;
171
+ }
172
+ // 保留样式元素(如果存在)
173
+ if (
174
+ stylesToApply &&
175
+ child instanceof HTMLStyleElement &&
176
+ child.getAttribute("data-wsx-light-component") ===
177
+ (this.config.styleName || this.constructor.name)
178
+ ) {
179
+ return false;
180
+ }
181
+ return true;
182
+ });
183
+ oldChildren.forEach((child) => child.remove());
184
+
185
+ // 确保样式元素在第一个位置
186
+ if (stylesToApply && this.children.length > 1) {
187
+ const styleElement = this.querySelector(
188
+ `style[data-wsx-light-component="${this.config.styleName || this.constructor.name}"]`
189
+ );
190
+ if (styleElement && styleElement !== this.firstChild) {
191
+ this.insertBefore(styleElement, this.firstChild);
192
+ }
193
+ }
194
+
195
+ // 恢复焦点状态(在 DOM 替换之后)
196
+ // 值已经在添加到 DOM 之前恢复了,这里只需要恢复焦点和光标位置
197
+ // 使用另一个 requestAnimationFrame 确保 DOM 已完全更新
198
+ requestAnimationFrame(() => {
199
+ this.restoreFocusState(focusState);
200
+ // 清除待处理的焦点状态
201
+ this._pendingFocusState = null;
202
+ });
203
+ });
138
204
  } catch (error) {
139
205
  logger.error(`[${this.constructor.name}] Error in rerender:`, error);
140
206
  this.renderError(error);
@@ -53,11 +53,24 @@ export function state(target: unknown, propertyKey: string | symbol | unknown):
53
53
  } else {
54
54
  const propertyKeyStr = String(propertyKey);
55
55
  if (propertyKeyStr === "[object Object]") {
56
- // Invalid propertyKey - likely a build configuration issue
56
+ // Invalid propertyKey - Babel plugin was not configured
57
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`
58
+ `@state decorator: Invalid propertyKey detected. ` +
59
+ `\n\n` +
60
+ `The @state decorator MUST be processed by Babel plugin at compile time. ` +
61
+ `It appears the Babel plugin is not configured in your build setup.` +
62
+ `\n\n` +
63
+ `To fix this, please:` +
64
+ `\n1. Install @wsxjs/wsx-vite-plugin: npm install @wsxjs/wsx-vite-plugin` +
65
+ `\n2. Configure it in vite.config.ts:` +
66
+ `\n import { wsx } from '@wsxjs/wsx-vite-plugin';` +
67
+ `\n export default defineConfig({ plugins: [wsx()] });` +
68
+ `\n3. Configure TypeScript (recommended: use @wsxjs/wsx-tsconfig):` +
69
+ `\n npm install --save-dev @wsxjs/wsx-tsconfig` +
70
+ `\n Then in tsconfig.json: { "extends": "@wsxjs/wsx-tsconfig/tsconfig.base.json" }` +
71
+ `\n Or manually: { "compilerOptions": { "experimentalDecorators": true, "useDefineForClassFields": false } }` +
72
+ `\n\n` +
73
+ `See: https://github.com/wsxjs/wsxjs#setup for more details.`
61
74
  );
62
75
  }
63
76
  normalizedPropertyKey = propertyKeyStr;
@@ -71,8 +84,22 @@ export function state(target: unknown, propertyKey: string | symbol | unknown):
71
84
  : normalizedPropertyKey.toString();
72
85
  throw new Error(
73
86
  `@state decorator: Cannot access property "${propertyKeyStr}". ` +
74
- `Target is ${target === null ? "null" : "undefined"}. ` +
75
- `Please ensure Babel plugin is configured in vite.config.ts`
87
+ `Target is ${target === null ? "null" : "undefined"}.` +
88
+ `\n\n` +
89
+ `The @state decorator MUST be processed by Babel plugin at compile time. ` +
90
+ `It appears the Babel plugin is not configured in your build setup.` +
91
+ `\n\n` +
92
+ `To fix this, please:` +
93
+ `\n1. Install @wsxjs/wsx-vite-plugin: npm install @wsxjs/wsx-vite-plugin` +
94
+ `\n2. Configure it in vite.config.ts:` +
95
+ `\n import { wsx } from '@wsxjs/wsx-vite-plugin';` +
96
+ `\n export default defineConfig({ plugins: [wsx()] });` +
97
+ `\n3. Configure TypeScript (recommended: use @wsxjs/wsx-tsconfig):` +
98
+ `\n npm install --save-dev @wsxjs/wsx-tsconfig` +
99
+ `\n Then in tsconfig.json: { "extends": "@wsxjs/wsx-tsconfig/tsconfig.base.json" }` +
100
+ `\n Or manually: { "compilerOptions": { "experimentalDecorators": true, "useDefineForClassFields": false } }` +
101
+ `\n\n` +
102
+ `See: https://github.com/wsxjs/wsxjs#setup for more details.`
76
103
  );
77
104
  }
78
105