@wsxjs/wsx-core 0.0.13 → 0.0.15

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
@@ -1208,14 +1208,45 @@ function deriveTagName(className, prefix) {
1208
1208
  // src/reactive-decorator.ts
1209
1209
  function state(targetOrContext, propertyKey) {
1210
1210
  let propertyName;
1211
- if (typeof targetOrContext === "object" && targetOrContext !== null && "kind" in targetOrContext && "name" in targetOrContext) {
1211
+ const propertyKeyIsObject = typeof propertyKey === "object" && propertyKey !== null;
1212
+ const targetIsObject = typeof targetOrContext === "object" && targetOrContext !== null;
1213
+ const hasStage3Indicators = targetIsObject && ("kind" in targetOrContext || "addInitializer" in targetOrContext || "access" in targetOrContext);
1214
+ const hasName = hasStage3Indicators && "name" in targetOrContext;
1215
+ const isStage3Decorator = propertyKeyIsObject || hasName || hasStage3Indicators && (propertyKey === void 0 || propertyKey === null) || targetIsObject && "addInitializer" in targetOrContext;
1216
+ if (isStage3Decorator) {
1212
1217
  const context = targetOrContext;
1213
- propertyName = typeof context.name === "string" ? context.name : context.name.toString();
1218
+ if (context.name) {
1219
+ propertyName = typeof context.name === "string" ? context.name : context.name.toString();
1220
+ } else if (propertyKeyIsObject) {
1221
+ if ("name" in propertyKey) {
1222
+ const keyObj = propertyKey;
1223
+ propertyName = keyObj.name ? typeof keyObj.name === "string" ? keyObj.name : keyObj.name.toString() : "unknown";
1224
+ } else if ("key" in propertyKey) {
1225
+ const keyObj = propertyKey;
1226
+ propertyName = keyObj.key ? typeof keyObj.key === "string" ? keyObj.key : keyObj.key.toString() : "unknown";
1227
+ } else {
1228
+ const keyStr = String(propertyKey);
1229
+ if (keyStr !== "[object Object]") {
1230
+ propertyName = keyStr;
1231
+ } else {
1232
+ propertyName = "unknown";
1233
+ }
1234
+ }
1235
+ } else {
1236
+ propertyName = "unknown";
1237
+ }
1214
1238
  } else {
1215
1239
  if (typeof propertyKey === "string" || typeof propertyKey === "symbol") {
1216
1240
  propertyName = typeof propertyKey === "string" ? propertyKey : propertyKey.toString();
1241
+ } else if (propertyKey != null) {
1242
+ const propertyKeyStr = String(propertyKey);
1243
+ if (propertyKeyStr === "[object Object]") {
1244
+ propertyName = "unknown";
1245
+ } else {
1246
+ propertyName = propertyKeyStr;
1247
+ }
1217
1248
  } else {
1218
- propertyName = String(propertyKey);
1249
+ propertyName = "unknown";
1219
1250
  }
1220
1251
  }
1221
1252
  console.warn(
@@ -1233,15 +1264,34 @@ To fix this and enable compile-time processing, please:
1233
1264
 
1234
1265
  See: https://github.com/wsxjs/wsxjs#setup for more details.`
1235
1266
  );
1236
- if (typeof targetOrContext === "object" && targetOrContext !== null && "kind" in targetOrContext && "name" in targetOrContext) {
1237
- const context = targetOrContext;
1238
- if (context.kind !== "field") {
1267
+ if (isStage3Decorator) {
1268
+ let context;
1269
+ if (hasStage3Indicators) {
1270
+ context = targetOrContext;
1271
+ } else if (propertyKeyIsObject) {
1272
+ const keyObj = propertyKey;
1273
+ const targetObj = targetOrContext;
1274
+ const nameValue = targetObj.name || keyObj.name || keyObj.key || "unknown";
1275
+ context = {
1276
+ kind: targetObj.kind || "field",
1277
+ name: nameValue,
1278
+ addInitializer: targetObj.addInitializer || keyObj.addInitializer,
1279
+ access: targetObj.access || keyObj.access
1280
+ };
1281
+ } else {
1282
+ context = targetOrContext;
1283
+ }
1284
+ if (context.kind && context.kind !== "field") {
1239
1285
  const nameStr = typeof context.name === "string" ? context.name : context.name.toString();
1240
1286
  throw new Error(
1241
1287
  `@state decorator can only be used on class fields, not ${context.kind}. Property: "${nameStr}"`
1242
1288
  );
1243
1289
  }
1244
- if (context.addInitializer) {
1290
+ if (!context.addInitializer) {
1291
+ console.warn(
1292
+ `[WSX] @state decorator: addInitializer not available for property "${propertyName}". The property will not be reactive. This usually means the decorator system is not properly configured.`
1293
+ );
1294
+ } else {
1245
1295
  context.addInitializer(function() {
1246
1296
  if (!this || typeof this.reactive !== "function" || typeof this.useState !== "function") {
1247
1297
  throw new Error(
@@ -1294,13 +1344,39 @@ The @state decorator can only be used in classes that extend WebComponent or Lig
1294
1344
  let normalizedPropertyKey;
1295
1345
  if (typeof propertyKey === "string" || typeof propertyKey === "symbol") {
1296
1346
  normalizedPropertyKey = propertyKey;
1297
- } else {
1347
+ } else if (propertyKey != null) {
1298
1348
  const propertyKeyStr = String(propertyKey);
1299
1349
  if (propertyKeyStr === "[object Object]") {
1350
+ if (typeof targetOrContext === "object" && targetOrContext !== null && ("kind" in targetOrContext || "addInitializer" in targetOrContext)) {
1351
+ console.warn(
1352
+ `[WSX] @state decorator: Detected potential Stage 3 decorator format but with unexpected propertyKey. This might be a compatibility issue. The decorator will attempt to work in runtime fallback mode.`
1353
+ );
1354
+ const context = targetOrContext;
1355
+ if (context.name) {
1356
+ const name = typeof context.name === "string" ? context.name : context.name.toString();
1357
+ throw new Error(
1358
+ `@state decorator: Detected Stage 3 decorator format but with invalid propertyKey. Property name: "${name}".
1359
+
1360
+ This usually means the decorator is being called in an unexpected format. Please ensure you have configured the Babel plugin correctly.
1361
+
1362
+ To fix this, please:
1363
+ 1. Install @wsxjs/wsx-vite-plugin: npm install @wsxjs/wsx-vite-plugin
1364
+ 2. Configure it in vite.config.ts:
1365
+ import { wsx } from '@wsxjs/wsx-vite-plugin';
1366
+ export default defineConfig({ plugins: [wsx()] });
1367
+ 3. Configure TypeScript (recommended: use @wsxjs/wsx-tsconfig):
1368
+ npm install --save-dev @wsxjs/wsx-tsconfig
1369
+ Then in tsconfig.json: { "extends": "@wsxjs/wsx-tsconfig/tsconfig.base.json" }
1370
+ Or manually: { "compilerOptions": { "experimentalDecorators": true, "useDefineForClassFields": false } }
1371
+
1372
+ See: https://github.com/wsxjs/wsxjs#setup for more details.`
1373
+ );
1374
+ }
1375
+ }
1300
1376
  throw new Error(
1301
- `@state decorator: Invalid propertyKey detected.
1377
+ `@state decorator: Invalid propertyKey detected (received object instead of string/symbol).
1302
1378
 
1303
- The @state decorator MUST be processed by Babel plugin at compile time. It appears the Babel plugin is not configured in your build setup.
1379
+ The @state decorator MUST be processed by Babel plugin at compile time. It appears the Babel plugin is not configured in your build setup.
1304
1380
 
1305
1381
  To fix this, please:
1306
1382
  1. Install @wsxjs/wsx-vite-plugin: npm install @wsxjs/wsx-vite-plugin
@@ -1316,6 +1392,10 @@ See: https://github.com/wsxjs/wsxjs#setup for more details.`
1316
1392
  );
1317
1393
  }
1318
1394
  normalizedPropertyKey = propertyKeyStr;
1395
+ } else {
1396
+ throw new Error(
1397
+ `@state decorator: propertyKey is missing. This usually means the decorator is not being called correctly. Please ensure you're using @state on a class field, not a method or other construct.`
1398
+ );
1319
1399
  }
1320
1400
  if (target == null) {
1321
1401
  const propertyKeyStr = typeof normalizedPropertyKey === "string" ? normalizedPropertyKey : normalizedPropertyKey.toString();
package/dist/index.mjs CHANGED
@@ -975,14 +975,45 @@ function deriveTagName(className, prefix) {
975
975
  // src/reactive-decorator.ts
976
976
  function state(targetOrContext, propertyKey) {
977
977
  let propertyName;
978
- if (typeof targetOrContext === "object" && targetOrContext !== null && "kind" in targetOrContext && "name" in targetOrContext) {
978
+ const propertyKeyIsObject = typeof propertyKey === "object" && propertyKey !== null;
979
+ const targetIsObject = typeof targetOrContext === "object" && targetOrContext !== null;
980
+ const hasStage3Indicators = targetIsObject && ("kind" in targetOrContext || "addInitializer" in targetOrContext || "access" in targetOrContext);
981
+ const hasName = hasStage3Indicators && "name" in targetOrContext;
982
+ const isStage3Decorator = propertyKeyIsObject || hasName || hasStage3Indicators && (propertyKey === void 0 || propertyKey === null) || targetIsObject && "addInitializer" in targetOrContext;
983
+ if (isStage3Decorator) {
979
984
  const context = targetOrContext;
980
- propertyName = typeof context.name === "string" ? context.name : context.name.toString();
985
+ if (context.name) {
986
+ propertyName = typeof context.name === "string" ? context.name : context.name.toString();
987
+ } else if (propertyKeyIsObject) {
988
+ if ("name" in propertyKey) {
989
+ const keyObj = propertyKey;
990
+ propertyName = keyObj.name ? typeof keyObj.name === "string" ? keyObj.name : keyObj.name.toString() : "unknown";
991
+ } else if ("key" in propertyKey) {
992
+ const keyObj = propertyKey;
993
+ propertyName = keyObj.key ? typeof keyObj.key === "string" ? keyObj.key : keyObj.key.toString() : "unknown";
994
+ } else {
995
+ const keyStr = String(propertyKey);
996
+ if (keyStr !== "[object Object]") {
997
+ propertyName = keyStr;
998
+ } else {
999
+ propertyName = "unknown";
1000
+ }
1001
+ }
1002
+ } else {
1003
+ propertyName = "unknown";
1004
+ }
981
1005
  } else {
982
1006
  if (typeof propertyKey === "string" || typeof propertyKey === "symbol") {
983
1007
  propertyName = typeof propertyKey === "string" ? propertyKey : propertyKey.toString();
1008
+ } else if (propertyKey != null) {
1009
+ const propertyKeyStr = String(propertyKey);
1010
+ if (propertyKeyStr === "[object Object]") {
1011
+ propertyName = "unknown";
1012
+ } else {
1013
+ propertyName = propertyKeyStr;
1014
+ }
984
1015
  } else {
985
- propertyName = String(propertyKey);
1016
+ propertyName = "unknown";
986
1017
  }
987
1018
  }
988
1019
  console.warn(
@@ -1000,15 +1031,34 @@ To fix this and enable compile-time processing, please:
1000
1031
 
1001
1032
  See: https://github.com/wsxjs/wsxjs#setup for more details.`
1002
1033
  );
1003
- if (typeof targetOrContext === "object" && targetOrContext !== null && "kind" in targetOrContext && "name" in targetOrContext) {
1004
- const context = targetOrContext;
1005
- if (context.kind !== "field") {
1034
+ if (isStage3Decorator) {
1035
+ let context;
1036
+ if (hasStage3Indicators) {
1037
+ context = targetOrContext;
1038
+ } else if (propertyKeyIsObject) {
1039
+ const keyObj = propertyKey;
1040
+ const targetObj = targetOrContext;
1041
+ const nameValue = targetObj.name || keyObj.name || keyObj.key || "unknown";
1042
+ context = {
1043
+ kind: targetObj.kind || "field",
1044
+ name: nameValue,
1045
+ addInitializer: targetObj.addInitializer || keyObj.addInitializer,
1046
+ access: targetObj.access || keyObj.access
1047
+ };
1048
+ } else {
1049
+ context = targetOrContext;
1050
+ }
1051
+ if (context.kind && context.kind !== "field") {
1006
1052
  const nameStr = typeof context.name === "string" ? context.name : context.name.toString();
1007
1053
  throw new Error(
1008
1054
  `@state decorator can only be used on class fields, not ${context.kind}. Property: "${nameStr}"`
1009
1055
  );
1010
1056
  }
1011
- if (context.addInitializer) {
1057
+ if (!context.addInitializer) {
1058
+ console.warn(
1059
+ `[WSX] @state decorator: addInitializer not available for property "${propertyName}". The property will not be reactive. This usually means the decorator system is not properly configured.`
1060
+ );
1061
+ } else {
1012
1062
  context.addInitializer(function() {
1013
1063
  if (!this || typeof this.reactive !== "function" || typeof this.useState !== "function") {
1014
1064
  throw new Error(
@@ -1061,13 +1111,39 @@ The @state decorator can only be used in classes that extend WebComponent or Lig
1061
1111
  let normalizedPropertyKey;
1062
1112
  if (typeof propertyKey === "string" || typeof propertyKey === "symbol") {
1063
1113
  normalizedPropertyKey = propertyKey;
1064
- } else {
1114
+ } else if (propertyKey != null) {
1065
1115
  const propertyKeyStr = String(propertyKey);
1066
1116
  if (propertyKeyStr === "[object Object]") {
1117
+ if (typeof targetOrContext === "object" && targetOrContext !== null && ("kind" in targetOrContext || "addInitializer" in targetOrContext)) {
1118
+ console.warn(
1119
+ `[WSX] @state decorator: Detected potential Stage 3 decorator format but with unexpected propertyKey. This might be a compatibility issue. The decorator will attempt to work in runtime fallback mode.`
1120
+ );
1121
+ const context = targetOrContext;
1122
+ if (context.name) {
1123
+ const name = typeof context.name === "string" ? context.name : context.name.toString();
1124
+ throw new Error(
1125
+ `@state decorator: Detected Stage 3 decorator format but with invalid propertyKey. Property name: "${name}".
1126
+
1127
+ This usually means the decorator is being called in an unexpected format. Please ensure you have configured the Babel plugin correctly.
1128
+
1129
+ To fix this, please:
1130
+ 1. Install @wsxjs/wsx-vite-plugin: npm install @wsxjs/wsx-vite-plugin
1131
+ 2. Configure it in vite.config.ts:
1132
+ import { wsx } from '@wsxjs/wsx-vite-plugin';
1133
+ export default defineConfig({ plugins: [wsx()] });
1134
+ 3. Configure TypeScript (recommended: use @wsxjs/wsx-tsconfig):
1135
+ npm install --save-dev @wsxjs/wsx-tsconfig
1136
+ Then in tsconfig.json: { "extends": "@wsxjs/wsx-tsconfig/tsconfig.base.json" }
1137
+ Or manually: { "compilerOptions": { "experimentalDecorators": true, "useDefineForClassFields": false } }
1138
+
1139
+ See: https://github.com/wsxjs/wsxjs#setup for more details.`
1140
+ );
1141
+ }
1142
+ }
1067
1143
  throw new Error(
1068
- `@state decorator: Invalid propertyKey detected.
1144
+ `@state decorator: Invalid propertyKey detected (received object instead of string/symbol).
1069
1145
 
1070
- The @state decorator MUST be processed by Babel plugin at compile time. It appears the Babel plugin is not configured in your build setup.
1146
+ The @state decorator MUST be processed by Babel plugin at compile time. It appears the Babel plugin is not configured in your build setup.
1071
1147
 
1072
1148
  To fix this, please:
1073
1149
  1. Install @wsxjs/wsx-vite-plugin: npm install @wsxjs/wsx-vite-plugin
@@ -1083,6 +1159,10 @@ See: https://github.com/wsxjs/wsxjs#setup for more details.`
1083
1159
  );
1084
1160
  }
1085
1161
  normalizedPropertyKey = propertyKeyStr;
1162
+ } else {
1163
+ throw new Error(
1164
+ `@state decorator: propertyKey is missing. This usually means the decorator is not being called correctly. Please ensure you're using @state on a class field, not a method or other construct.`
1165
+ );
1086
1166
  }
1087
1167
  if (target == null) {
1088
1168
  const propertyKeyStr = typeof normalizedPropertyKey === "string" ? normalizedPropertyKey : normalizedPropertyKey.toString();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wsxjs/wsx-core",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "Core WSX Framework - Web Components with JSX syntax",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -119,7 +119,7 @@ export abstract class BaseComponent extends HTMLElement {
119
119
  *
120
120
  * @returns JSX元素
121
121
  */
122
- abstract render(): HTMLElement;
122
+ abstract render(): HTMLElement | SVGElement;
123
123
 
124
124
  /**
125
125
  * 可选生命周期钩子:组件已连接
@@ -34,10 +34,16 @@ export abstract class LightComponent extends BaseComponent {
34
34
  *
35
35
  * @returns JSX元素
36
36
  */
37
- abstract render(): HTMLElement;
37
+ abstract render(): HTMLElement | SVGElement;
38
38
 
39
39
  /**
40
40
  * Web Component生命周期:连接到DOM
41
+ *
42
+ * 渲染策略:
43
+ * 1. 检查组件中是否已有实际内容(排除样式元素)
44
+ * 2. 如果有内容且完整,跳过渲染(避免重复元素和性能优化)
45
+ * 3. 如果没有内容或不完整,清空后重新渲染
46
+ * 4. 样式元素需要保留
41
47
  */
42
48
  connectedCallback(): void {
43
49
  this.connected = true;
@@ -48,19 +54,57 @@ export abstract class LightComponent extends BaseComponent {
48
54
  // So we need to check _autoStyles directly first, then fallback to config.styles getter
49
55
  // The getter will dynamically check _autoStyles when accessed
50
56
  const stylesToApply = this._autoStyles || this.config.styles;
57
+ const styleName = this.config.styleName || this.constructor.name;
51
58
  if (stylesToApply) {
52
- const styleName = this.config.styleName || this.constructor.name;
53
59
  this.applyScopedStyles(styleName, stylesToApply);
54
60
  }
55
61
 
56
- // 渲染JSX内容到Light DOM
57
- const content = this.render();
58
- this.appendChild(content);
62
+ // 检查是否有实际内容(排除样式元素)
63
+ // 错误元素:如果存在错误信息,需要重新渲染以恢复正常
64
+ const styleElement = this.querySelector(
65
+ `style[data-wsx-light-component="${styleName}"]`
66
+ ) as HTMLStyleElement | null;
67
+ const hasErrorElement = Array.from(this.children).some(
68
+ (child) =>
69
+ child instanceof HTMLElement &&
70
+ child !== styleElement &&
71
+ child.style.color === "red" &&
72
+ child.textContent?.includes("Component Error")
73
+ );
74
+ const hasActualContent = Array.from(this.children).some(
75
+ (child) => child !== styleElement
76
+ );
77
+
78
+ // 如果有错误元素,需要重新渲染以恢复正常
79
+ // 如果有实际内容且没有错误,跳过渲染(避免重复元素)
80
+ if (hasActualContent && !hasErrorElement) {
81
+ // 已经有内容且没有错误,跳过渲染(避免重复元素)
82
+ // 但确保样式元素在正确位置
83
+ if (styleElement && styleElement !== this.firstChild) {
84
+ this.insertBefore(styleElement, this.firstChild);
85
+ }
86
+ } else {
87
+ // 没有内容,需要渲染
88
+ // 清空旧内容(保留样式元素)
89
+ const childrenToRemove = Array.from(this.children).filter(
90
+ (child) => child !== styleElement
91
+ );
92
+ childrenToRemove.forEach((child) => child.remove());
93
+
94
+ // 渲染JSX内容到Light DOM
95
+ const content = this.render();
96
+ this.appendChild(content);
97
+
98
+ // 确保样式元素在第一个位置(如果存在)
99
+ if (styleElement && styleElement !== this.firstChild) {
100
+ this.insertBefore(styleElement, this.firstChild);
101
+ }
102
+ }
59
103
 
60
- // 初始化事件监听器
104
+ // 初始化事件监听器(无论是否渲染,都需要重新初始化,因为 DOM 可能被移动)
61
105
  this.initializeEventListeners();
62
106
 
63
- // 调用子类的初始化钩子
107
+ // 调用子类的初始化钩子(无论是否渲染,都需要调用,因为组件已连接)
64
108
  this.onConnected?.();
65
109
  } catch (error) {
66
110
  logger.error(`[${this.constructor.name}] Error in connectedCallback:`, error);
@@ -48,241 +48,136 @@ interface DecoratorContext {
48
48
  addInitializer?(initializer: () => void): void;
49
49
  }
50
50
 
51
+ /**
52
+ * Helper function to create error message for missing Babel plugin
53
+ */
54
+ function createBabelPluginError(propertyName: string): string {
55
+ return (
56
+ `@state decorator on property "${propertyName}" MUST be processed by Babel plugin at compile time. ` +
57
+ `It appears the Babel plugin is not configured in your build setup. ` +
58
+ `\n\n` +
59
+ `The @state decorator cannot work without the Babel plugin. ` +
60
+ `\n\n` +
61
+ `To fix this, please:` +
62
+ `\n1. Install @wsxjs/wsx-vite-plugin: npm install @wsxjs/wsx-vite-plugin` +
63
+ `\n2. Configure it in vite.config.ts:` +
64
+ `\n import { wsx } from '@wsxjs/wsx-vite-plugin';` +
65
+ `\n export default defineConfig({ plugins: [wsx()] });` +
66
+ `\n3. Configure TypeScript (recommended: use @wsxjs/wsx-tsconfig):` +
67
+ `\n npm install --save-dev @wsxjs/wsx-tsconfig` +
68
+ `\n Then in tsconfig.json: { "extends": "@wsxjs/wsx-tsconfig/tsconfig.base.json" }` +
69
+ `\n Or manually: { "compilerOptions": { "experimentalDecorators": true, "useDefineForClassFields": false } }` +
70
+ `\n\n` +
71
+ `See: https://github.com/wsxjs/wsxjs#setup for more details.`
72
+ );
73
+ }
74
+
51
75
  export function state(
52
76
  targetOrContext: unknown | DecoratorContext,
53
77
  propertyKey?: string | symbol | unknown
54
- ): unknown {
78
+ // Compatibility with Babel plugin which is required for this decorator to work properly
79
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
80
+ ): any {
55
81
  /**
56
- * @state decorator supports both:
57
- * 1. Compile-time processing by Babel plugin (preferred) - decorator is removed at compile time
58
- * 2. Runtime fallback when Babel plugin is not configured - decorator executes at runtime
59
- *
82
+ * @state decorator MUST be processed by Babel plugin at compile time.
60
83
  * If this function is executed at runtime, it means Babel plugin did not process it.
61
- * We should warn the user and provide fallback functionality.
84
+ * We MUST throw an error - no fallback is allowed.
62
85
  */
63
86
 
64
- // Determine property name for warning message
65
- let propertyName: string;
66
- if (
67
- typeof targetOrContext === "object" &&
68
- targetOrContext !== null &&
69
- "kind" in targetOrContext &&
70
- "name" in targetOrContext
71
- ) {
72
- // Stage 3 decorator format
73
- const context = targetOrContext as DecoratorContext;
74
- propertyName = typeof context.name === "string" ? context.name : context.name.toString();
75
- } else {
76
- // Legacy decorator format
77
- if (typeof propertyKey === "string" || typeof propertyKey === "symbol") {
78
- propertyName = typeof propertyKey === "string" ? propertyKey : propertyKey.toString();
79
- } else {
80
- propertyName = String(propertyKey);
81
- }
82
- }
83
-
84
- // Show warning immediately when decorator is executed at runtime
85
- // This means Babel plugin did not process it
86
- console.warn(
87
- `[WSX] @state decorator is using runtime fallback. ` +
88
- `Property "${propertyName}" will work but with reduced performance. ` +
89
- `\n\n` +
90
- `To fix this and enable compile-time processing, please:` +
91
- `\n1. Install @wsxjs/wsx-vite-plugin: npm install @wsxjs/wsx-vite-plugin` +
92
- `\n2. Configure it in vite.config.ts:` +
93
- `\n import { wsx } from '@wsxjs/wsx-vite-plugin';` +
94
- `\n export default defineConfig({ plugins: [wsx()] });` +
95
- `\n3. Configure TypeScript (recommended: use @wsxjs/wsx-tsconfig):` +
96
- `\n npm install --save-dev @wsxjs/wsx-tsconfig` +
97
- `\n Then in tsconfig.json: { "extends": "@wsxjs/wsx-tsconfig/tsconfig.base.json" }` +
98
- `\n Or manually: { "compilerOptions": { "experimentalDecorators": true, "useDefineForClassFields": false } }` +
99
- `\n\n` +
100
- `See: https://github.com/wsxjs/wsxjs#setup for more details.`
101
- );
87
+ // Determine property name for error message
88
+ let propertyName: string = "unknown";
102
89
 
103
90
  // Check if this is a Stage 3 decorator (context object)
104
- if (
105
- typeof targetOrContext === "object" &&
106
- targetOrContext !== null &&
107
- "kind" in targetOrContext &&
108
- "name" in targetOrContext
109
- ) {
110
- const context = targetOrContext as DecoratorContext;
111
-
112
- // Only support field decorators
113
- if (context.kind !== "field") {
114
- const nameStr =
115
- typeof context.name === "string" ? context.name : context.name.toString();
116
- throw new Error(
117
- `@state decorator can only be used on class fields, not ${context.kind}. Property: "${nameStr}"`
118
- );
119
- }
120
-
121
- // Runtime fallback: Use addInitializer to set up reactive state
122
- if (context.addInitializer) {
123
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
124
- context.addInitializer(function (this: any) {
125
- // 'this' refers to the component instance
126
- if (
127
- !this ||
128
- typeof this.reactive !== "function" ||
129
- typeof this.useState !== "function"
130
- ) {
131
- throw new Error(
132
- `@state decorator runtime fallback: Component does not extend WebComponent or LightComponent. ` +
133
- `Property "${propertyName}" cannot be made reactive. ` +
134
- `\n\n` +
135
- `The @state decorator can only be used in classes that extend WebComponent or LightComponent. ` +
136
- `Please ensure your component class extends one of these base classes.`
137
- );
91
+ const propertyKeyIsObject = typeof propertyKey === "object" && propertyKey !== null;
92
+ const targetIsObject = typeof targetOrContext === "object" && targetOrContext !== null;
93
+
94
+ const hasStage3Indicators =
95
+ targetIsObject &&
96
+ ("kind" in targetOrContext ||
97
+ "addInitializer" in targetOrContext ||
98
+ "access" in targetOrContext);
99
+
100
+ const hasName = hasStage3Indicators && "name" in targetOrContext;
101
+
102
+ const isStage3Decorator =
103
+ propertyKeyIsObject ||
104
+ hasName ||
105
+ (hasStage3Indicators && (propertyKey === undefined || propertyKey === null)) ||
106
+ (targetIsObject && "addInitializer" in targetOrContext);
107
+
108
+ if (isStage3Decorator) {
109
+ // Stage 3 decorator format - determine property name safely
110
+ if (hasStage3Indicators && targetOrContext && typeof targetOrContext === "object") {
111
+ const context = targetOrContext as DecoratorContext;
112
+ if (context.name) {
113
+ propertyName =
114
+ typeof context.name === "string" ? context.name : context.name.toString();
115
+ }
116
+ } else if (propertyKeyIsObject) {
117
+ // If propertyKey is an object, try to extract name from it
118
+ const keyObj = propertyKey as Record<string, unknown>;
119
+ const targetObj = targetOrContext as Record<string, unknown> | null;
120
+
121
+ // Try to get name from multiple sources
122
+ const nameValue =
123
+ (targetObj?.name as string | symbol | undefined) ||
124
+ (keyObj.name as string | symbol | undefined) ||
125
+ (keyObj.key as string | symbol | undefined);
126
+
127
+ if (nameValue) {
128
+ propertyName = typeof nameValue === "string" ? nameValue : nameValue.toString();
129
+ } else {
130
+ const keyStr = String(propertyKey);
131
+ if (keyStr !== "[object Object]") {
132
+ propertyName = keyStr;
138
133
  }
134
+ }
135
+ }
139
136
 
140
- // Get initial value - try from access.get() first, then from property
141
- let initialValue: unknown = undefined;
142
- if (context.access?.get) {
143
- try {
144
- initialValue = context.access.get();
145
- } catch {
146
- // Access might not be available yet, try property directly
147
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
148
- initialValue = (this as any)[propertyName];
149
- }
150
- } else {
151
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
152
- initialValue = (this as any)[propertyName];
153
- }
154
-
155
- // Determine if it's an object/array
156
- const isObject =
157
- initialValue !== null &&
158
- initialValue !== undefined &&
159
- (typeof initialValue === "object" || Array.isArray(initialValue));
160
-
161
- if (isObject) {
162
- // For objects/arrays: use reactive()
163
- let reactiveValue = this.reactive(initialValue);
164
- Object.defineProperty(this, propertyName, {
165
- get: () => reactiveValue,
166
- set: (newValue: unknown) => {
167
- // Auto-wrap new values in reactive if they're objects/arrays
168
- if (
169
- newValue !== null &&
170
- newValue !== undefined &&
171
- (typeof newValue === "object" || Array.isArray(newValue))
172
- ) {
173
- // Create new reactive value
174
- reactiveValue = this.reactive(newValue);
175
- this.scheduleRerender();
176
- } else {
177
- // For primitives, just assign (but this shouldn't happen for object properties)
178
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
179
- reactiveValue = newValue as any;
180
- this.scheduleRerender();
181
- }
182
- },
183
- enumerable: true,
184
- configurable: true,
185
- });
186
- } else {
187
- // For primitives: use useState
188
- const [getState, setState] = this.useState(propertyName, initialValue);
189
- Object.defineProperty(this, propertyName, {
190
- get: getState,
191
- set: setState,
192
- enumerable: true,
193
- configurable: true,
194
- });
195
- }
196
- });
137
+ // Only support field decorators (if kind is specified)
138
+ if (targetIsObject && "kind" in targetOrContext) {
139
+ const context = targetOrContext as DecoratorContext;
140
+ if (context.kind && context.kind !== "field") {
141
+ const nameStr =
142
+ typeof context.name === "string" ? context.name : context.name.toString();
143
+ throw new Error(
144
+ `@state decorator can only be used on class fields, not ${context.kind}. Property: "${nameStr}"`
145
+ );
146
+ }
197
147
  }
198
148
 
199
- // Return context for Stage 3 decorators
200
- return context;
149
+ // If we reach here, the decorator is being executed at runtime
150
+ // This means Babel plugin did not process it - throw error immediately
151
+ throw new Error(createBabelPluginError(propertyName));
201
152
  }
202
153
 
203
154
  // Legacy decorator format (experimentalDecorators: true)
204
155
  // This should ideally be removed by Babel plugin
205
- const target = targetOrContext;
206
- let normalizedPropertyKey: string | symbol;
207
156
  if (typeof propertyKey === "string" || typeof propertyKey === "symbol") {
208
- normalizedPropertyKey = propertyKey;
209
- } else {
157
+ propertyName = typeof propertyKey === "string" ? propertyKey : propertyKey.toString();
158
+ } else if (propertyKey != null) {
210
159
  const propertyKeyStr = String(propertyKey);
211
160
  if (propertyKeyStr === "[object Object]") {
212
- // Invalid propertyKey - Babel plugin was not configured
213
- throw new Error(
214
- `@state decorator: Invalid propertyKey detected. ` +
215
- `\n\n` +
216
- `The @state decorator MUST be processed by Babel plugin at compile time. ` +
217
- `It appears the Babel plugin is not configured in your build setup.` +
218
- `\n\n` +
219
- `To fix this, please:` +
220
- `\n1. Install @wsxjs/wsx-vite-plugin: npm install @wsxjs/wsx-vite-plugin` +
221
- `\n2. Configure it in vite.config.ts:` +
222
- `\n import { wsx } from '@wsxjs/wsx-vite-plugin';` +
223
- `\n export default defineConfig({ plugins: [wsx()] });` +
224
- `\n3. Configure TypeScript (recommended: use @wsxjs/wsx-tsconfig):` +
225
- `\n npm install --save-dev @wsxjs/wsx-tsconfig` +
226
- `\n Then in tsconfig.json: { "extends": "@wsxjs/wsx-tsconfig/tsconfig.base.json" }` +
227
- `\n Or manually: { "compilerOptions": { "experimentalDecorators": true, "useDefineForClassFields": false } }` +
228
- `\n\n` +
229
- `See: https://github.com/wsxjs/wsxjs#setup for more details.`
230
- );
161
+ // Invalid propertyKey - this might happen if decorator is called in unexpected format
162
+ // Check if targetOrContext might actually be a Stage 3 context that we missed
163
+ if (
164
+ typeof targetOrContext === "object" &&
165
+ targetOrContext !== null &&
166
+ ("kind" in targetOrContext || "addInitializer" in targetOrContext)
167
+ ) {
168
+ // This looks like it might be a Stage 3 decorator that we didn't detect properly
169
+ const context = targetOrContext as Partial<DecoratorContext>;
170
+ if (context.name) {
171
+ propertyName =
172
+ typeof context.name === "string" ? context.name : context.name.toString();
173
+ }
174
+ }
175
+ } else {
176
+ propertyName = propertyKeyStr;
231
177
  }
232
- normalizedPropertyKey = propertyKeyStr;
233
- }
234
-
235
- // Basic validation: ensure target is valid
236
- if (target == null) {
237
- const propertyKeyStr =
238
- typeof normalizedPropertyKey === "string"
239
- ? normalizedPropertyKey
240
- : normalizedPropertyKey.toString();
241
- throw new Error(
242
- `@state decorator: Cannot access property "${propertyKeyStr}". ` +
243
- `Target is ${target === null ? "null" : "undefined"}.` +
244
- `\n\n` +
245
- `The @state decorator MUST be processed by Babel plugin at compile time. ` +
246
- `It appears the Babel plugin is not configured in your build setup.` +
247
- `\n\n` +
248
- `To fix this, please:` +
249
- `\n1. Install @wsxjs/wsx-vite-plugin: npm install @wsxjs/wsx-vite-plugin` +
250
- `\n2. Configure it in vite.config.ts:` +
251
- `\n import { wsx } from '@wsxjs/wsx-vite-plugin';` +
252
- `\n export default defineConfig({ plugins: [wsx()] });` +
253
- `\n3. Configure TypeScript (recommended: use @wsxjs/wsx-tsconfig):` +
254
- `\n npm install --save-dev @wsxjs/wsx-tsconfig` +
255
- `\n Then in tsconfig.json: { "extends": "@wsxjs/wsx-tsconfig/tsconfig.base.json" }` +
256
- `\n Or manually: { "compilerOptions": { "experimentalDecorators": true, "useDefineForClassFields": false } }` +
257
- `\n\n` +
258
- `See: https://github.com/wsxjs/wsxjs#setup for more details.`
259
- );
260
- }
261
-
262
- if (typeof target !== "object") {
263
- const propertyKeyStr =
264
- typeof normalizedPropertyKey === "string"
265
- ? normalizedPropertyKey
266
- : normalizedPropertyKey.toString();
267
- throw new Error(
268
- `@state decorator: Cannot be used on "${propertyKeyStr}". ` +
269
- `@state is for properties only, not methods.`
270
- );
271
- }
272
-
273
- // Validate that property has an initial value
274
- const descriptor = Object.getOwnPropertyDescriptor(target, normalizedPropertyKey);
275
- if (descriptor?.get) {
276
- const propertyKeyStr =
277
- typeof normalizedPropertyKey === "string"
278
- ? normalizedPropertyKey
279
- : normalizedPropertyKey.toString();
280
- throw new Error(
281
- `@state decorator cannot be used with getter properties. Property: "${propertyKeyStr}"`
282
- );
283
178
  }
284
179
 
285
- // Note: We don't store metadata or remove the property here.
286
- // Babel plugin handles everything at compile time.
287
- // If this function is called at runtime, it means Babel plugin didn't process the decorator.
180
+ // If we reach here, the decorator is being executed at runtime
181
+ // This means Babel plugin did not process it - throw error immediately
182
+ throw new Error(createBabelPluginError(propertyName));
288
183
  }
@@ -40,25 +40,66 @@ export abstract class WebComponent extends BaseComponent {
40
40
  *
41
41
  * @returns JSX元素
42
42
  */
43
- abstract render(): HTMLElement;
43
+ abstract render(): HTMLElement | SVGElement;
44
44
 
45
45
  /**
46
46
  * Web Component生命周期:连接到DOM
47
+ *
48
+ * 渲染策略:
49
+ * 1. 检查 Shadow DOM 中是否已有实际内容(排除样式和 slot)
50
+ * 2. 如果有内容,先清空再渲染(避免重复元素)
51
+ * 3. 如果没有内容,直接渲染
52
+ * 4. Slot 元素会被重新添加,浏览器会自动将 Light DOM 中的内容分配到 slot
47
53
  */
48
54
  connectedCallback(): void {
49
55
  this.connected = true;
50
56
  try {
51
- // 应用CSS样式到Shadow DOM
52
- // Check both _autoStyles getter and config.styles getter
57
+ // 应用CSS样式到Shadow DOM(先应用,因为样式可能使用 fallback 添加 style 元素)
53
58
  const stylesToApply = this._autoStyles || this.config.styles;
54
59
  if (stylesToApply) {
55
60
  const styleName = this.config.styleName || this.constructor.name;
56
61
  StyleManager.applyStyles(this.shadowRoot, styleName, stylesToApply);
57
62
  }
58
63
 
59
- // 渲染JSX内容到Shadow DOM
60
- const content = this.render();
61
- this.shadowRoot.appendChild(content);
64
+ // 检查是否有实际内容(排除样式和 slot)
65
+ // 样式元素:可能由 StyleManager fallback 添加
66
+ // Slot 元素:不算"内容",因为 slot 的内容在 Light DOM 中
67
+ // 错误元素:如果存在错误信息,需要重新渲染以恢复正常
68
+ const allChildren = Array.from(this.shadowRoot.children);
69
+ const styleElements = allChildren.filter((child) => child instanceof HTMLStyleElement);
70
+ const slotElements = allChildren.filter((child) => child instanceof HTMLSlotElement);
71
+ const hasErrorElement = allChildren.some(
72
+ (child) =>
73
+ child instanceof HTMLElement &&
74
+ child.style.color === "red" &&
75
+ child.textContent?.includes("Component Error")
76
+ );
77
+ const hasActualContent =
78
+ allChildren.length > styleElements.length + slotElements.length;
79
+
80
+ // 如果有错误元素,需要重新渲染以恢复正常
81
+ // 如果有实际内容且没有错误,跳过渲染(避免重复元素)
82
+ if (hasActualContent && !hasErrorElement) {
83
+ // 已经有内容且没有错误,跳过渲染(避免重复元素)
84
+ // 样式已在上方应用(StyleManager.applyStyles 是幂等的)
85
+ // Slot 元素已存在,浏览器会自动将 Light DOM 中的内容分配到 slot
86
+ } else {
87
+ // 没有内容,需要渲染
88
+ // 清空 Shadow DOM(包括可能的旧内容)
89
+ this.shadowRoot.innerHTML = "";
90
+
91
+ // 重新应用样式(因为上面清空了)
92
+ if (stylesToApply) {
93
+ const styleName = this.config.styleName || this.constructor.name;
94
+ StyleManager.applyStyles(this.shadowRoot, styleName, stylesToApply);
95
+ }
96
+
97
+ // 渲染JSX内容到Shadow DOM
98
+ // render() 应该返回包含 slot 元素的内容(如果需要)
99
+ // 浏览器会自动将 Light DOM 中的内容分配到 slot
100
+ const content = this.render();
101
+ this.shadowRoot.appendChild(content);
102
+ }
62
103
 
63
104
  // 初始化事件监听器
64
105
  this.initializeEventListeners();