@wsxjs/eslint-plugin-wsx 0.0.11 → 0.0.13

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/README.md CHANGED
@@ -2,6 +2,222 @@
2
2
 
3
3
  ESLint plugin for WSX Framework - enforces best practices and framework-specific rules for Web Components with JSX.
4
4
 
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install --save-dev @wsxjs/eslint-plugin-wsx
9
+ # or
10
+ pnpm add -D @wsxjs/eslint-plugin-wsx
11
+ # or
12
+ yarn add -D @wsxjs/eslint-plugin-wsx
13
+ ```
14
+
15
+ ## Setup
16
+
17
+ ### ESLint 9+ (Flat Config)
18
+
19
+ Create or update `eslint.config.js` (or `eslint.config.mjs`):
20
+
21
+ ```javascript
22
+ import js from "@eslint/js";
23
+ import typescript from "@typescript-eslint/eslint-plugin";
24
+ import typescriptParser from "@typescript-eslint/parser";
25
+ import wsxPlugin from "@wsxjs/eslint-plugin-wsx";
26
+ import globals from "globals";
27
+
28
+ export default [
29
+ {
30
+ ignores: ["**/dist/", "**/node_modules/"],
31
+ },
32
+ js.configs.recommended,
33
+ {
34
+ files: ["**/*.{ts,tsx,js,jsx,wsx}"],
35
+ languageOptions: {
36
+ parser: typescriptParser,
37
+ parserOptions: {
38
+ ecmaVersion: "latest",
39
+ sourceType: "module",
40
+ ecmaFeatures: {
41
+ jsx: true,
42
+ },
43
+ jsxPragma: "h",
44
+ jsxFragmentName: "Fragment",
45
+ experimentalDecorators: true, // Required for @state decorator
46
+ extraFileExtensions: [".wsx"],
47
+ },
48
+ globals: {
49
+ ...globals.browser,
50
+ ...globals.es2021,
51
+ h: "readonly",
52
+ Fragment: "readonly",
53
+ },
54
+ },
55
+ plugins: {
56
+ "@typescript-eslint": typescript,
57
+ wsx: wsxPlugin,
58
+ },
59
+ rules: {
60
+ ...typescript.configs.recommended.rules,
61
+ "@typescript-eslint/no-explicit-any": "warn",
62
+ "@typescript-eslint/no-unused-vars": [
63
+ "error",
64
+ {
65
+ argsIgnorePattern: "^_",
66
+ varsIgnorePattern: "^_",
67
+ },
68
+ ],
69
+ // WSX plugin rules
70
+ "wsx/render-method-required": "error",
71
+ "wsx/no-react-imports": "error",
72
+ "wsx/web-component-naming": "warn",
73
+ "wsx/state-requires-initial-value": "error",
74
+ "no-undef": "off", // TypeScript handles this
75
+ },
76
+ },
77
+ ];
78
+ ```
79
+
80
+ ### Required Dependencies
81
+
82
+ Make sure you have these peer dependencies installed:
83
+
84
+ ```bash
85
+ npm install --save-dev eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser globals
86
+ ```
87
+
88
+ **Important**: The `experimentalDecorators: true` option in `parserOptions` is **required** for the `wsx/state-requires-initial-value` rule to work correctly. Without it, ESLint cannot parse `@state` decorators and the rule will not detect violations.
89
+
90
+ ## Rules
91
+
92
+ ### `wsx/render-method-required`
93
+
94
+ **Error level**: `error`
95
+
96
+ Ensures WSX components implement the required `render()` method.
97
+
98
+ **Invalid**:
99
+ ```typescript
100
+ class MyComponent extends WebComponent {
101
+ // Missing render() method
102
+ }
103
+ ```
104
+
105
+ **Valid**:
106
+ ```typescript
107
+ class MyComponent extends WebComponent {
108
+ render() {
109
+ return <div>Hello</div>;
110
+ }
111
+ }
112
+ ```
113
+
114
+ ### `wsx/no-react-imports`
115
+
116
+ **Error level**: `error`
117
+
118
+ Prevents React imports in WSX files. WSX uses its own JSX runtime.
119
+
120
+ **Invalid**:
121
+ ```typescript
122
+ import React from "react"; // ❌
123
+ import { useState } from "react"; // ❌
124
+ ```
125
+
126
+ **Valid**:
127
+ ```typescript
128
+ import { WebComponent, state } from "@wsxjs/wsx-core"; // ✅
129
+ ```
130
+
131
+ ### `wsx/web-component-naming`
132
+
133
+ **Error level**: `warn`
134
+
135
+ Enforces proper Web Component tag naming conventions (kebab-case with at least one hyphen).
136
+
137
+ **Invalid**:
138
+ ```typescript
139
+ @autoRegister({ tagName: "mycomponent" }) // ❌ Missing hyphen
140
+ @autoRegister({ tagName: "MyComponent" }) // ❌ Not kebab-case
141
+ ```
142
+
143
+ **Valid**:
144
+ ```typescript
145
+ @autoRegister({ tagName: "my-component" }) // ✅
146
+ @autoRegister({ tagName: "wsx-button" }) // ✅
147
+ ```
148
+
149
+ ### `wsx/state-requires-initial-value`
150
+
151
+ **Error level**: `error`
152
+
153
+ Requires `@state` decorator properties to have initial values. This is mandatory because:
154
+ 1. The Babel plugin needs the initial value to determine if it's a primitive (uses `useState`) or object/array (uses `reactive`)
155
+ 2. Without an initial value, the decorator cannot be properly transformed at compile time
156
+ 3. The runtime fallback also requires an initial value to set up reactive state correctly
157
+
158
+ **Invalid**:
159
+ ```typescript
160
+ class MyComponent extends WebComponent {
161
+ @state private maskStrokeColor?: string; // ❌ Missing initial value
162
+ @state private count; // ❌ Missing initial value
163
+ @state private user; // ❌ Missing initial value
164
+ }
165
+ ```
166
+
167
+ **Valid**:
168
+ ```typescript
169
+ class MyComponent extends WebComponent {
170
+ @state private maskStrokeColor = ""; // ✅ String
171
+ @state private count = 0; // ✅ Number
172
+ @state private enabled = false; // ✅ Boolean
173
+ @state private user = { name: "John" }; // ✅ Object
174
+ @state private items = []; // ✅ Array
175
+ @state private optional: string | undefined = undefined; // ✅ Optional with explicit undefined
176
+ @state private size?: number = 32; // ✅ Optional with default value
177
+ }
178
+ ```
179
+
180
+ **Error Message Example**:
181
+ ```
182
+ @state decorator on property 'size' requires an initial value.
183
+
184
+ Examples:
185
+ @state private size = ''; // for string
186
+ @state private size = 0; // for number
187
+ @state private size = {}; // for object
188
+ @state private size = []; // for array
189
+ @state private size = undefined; // for optional
190
+ ```
191
+
192
+ ## Configuration Options
193
+
194
+ ### Disable Specific Rules
195
+
196
+ If you need to disable a specific rule:
197
+
198
+ ```javascript
199
+ {
200
+ rules: {
201
+ "wsx/web-component-naming": "off", // Disable naming rule
202
+ "wsx/state-requires-initial-value": "warn", // Change to warning
203
+ },
204
+ }
205
+ ```
206
+
207
+ ### File-Specific Rules
208
+
209
+ Apply rules only to `.wsx` files:
210
+
211
+ ```javascript
212
+ {
213
+ files: ["**/*.wsx"],
214
+ rules: {
215
+ "wsx/render-method-required": "error",
216
+ "wsx/no-react-imports": "error",
217
+ },
218
+ }
219
+ ```
220
+
5
221
  ## Testing Results
6
222
 
7
223
  ✅ **38 tests passed** with **100% code coverage**
@@ -38,6 +254,7 @@ This plugin now uses industry-standard testing practices:
38
254
  - 🔍 **render-method-required**: Ensures WSX components implement the required `render()` method
39
255
  - 🚫 **no-react-imports**: Prevents React imports in WSX files
40
256
  - 🏷️ **web-component-naming**: Enforces proper Web Component tag naming conventions
257
+ - ✅ **state-requires-initial-value**: Requires `@state` decorator properties to have initial values
41
258
 
42
259
  ## Framework Integration
43
260
 
package/dist/index.d.mts CHANGED
@@ -21,6 +21,7 @@ interface WSXConfig {
21
21
  };
22
22
  jsxPragma?: string;
23
23
  jsxFragmentName?: string;
24
+ experimentalDecorators?: boolean;
24
25
  };
25
26
  plugins?: string[];
26
27
  rules?: Record<string, unknown>;
package/dist/index.d.ts CHANGED
@@ -21,6 +21,7 @@ interface WSXConfig {
21
21
  };
22
22
  jsxPragma?: string;
23
23
  jsxFragmentName?: string;
24
+ experimentalDecorators?: boolean;
24
25
  };
25
26
  plugins?: string[];
26
27
  rules?: Record<string, unknown>;
package/dist/index.js CHANGED
@@ -184,110 +184,74 @@ var webComponentNaming = {
184
184
  }
185
185
  };
186
186
 
187
- // src/rules/no-state-on-html-attributes.ts
188
- var noStateOnHtmlAttributes = {
187
+ // src/rules/state-requires-initial-value.ts
188
+ var stateRequiresInitialValue = {
189
189
  meta: {
190
190
  type: "problem",
191
191
  docs: {
192
- description: "disallow @state decorator on HTML attributes",
192
+ description: "require @state decorator properties to have initial values",
193
193
  category: "Possible Errors",
194
194
  recommended: true
195
195
  },
196
196
  messages: {
197
- stateOnHtmlAttribute: "@state decorator cannot be used on properties that are HTML attributes. Property '{{propertyName}}' is defined in observedAttributes. HTML attributes should be handled via onAttributeChanged, not @state decorator."
197
+ missingInitialValue: "@state decorator on property '{{propertyName}}' requires an initial value.\n\nExamples:\n @state private {{propertyName}} = ''; // for string\n @state private {{propertyName}} = 0; // for number\n @state private {{propertyName}} = {}; // for object\n @state private {{propertyName}} = []; // for array\n @state private {{propertyName}} = undefined; // for optional"
198
198
  },
199
199
  schema: []
200
200
  },
201
201
  create(context) {
202
- const classObservedAttributes = /* @__PURE__ */ new Map();
203
202
  const stateImports = /* @__PURE__ */ new Set();
204
203
  return {
205
- // Track imports
206
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
204
+ // Track imports to identify @state decorator
207
205
  ImportDeclaration(node) {
208
- if (node.source.type === "StringLiteral" && (node.source.value === "@wsxjs/wsx-core" || node.source.value.endsWith("/wsx-core"))) {
209
- node.specifiers.forEach((spec) => {
210
- if (spec.type === "ImportSpecifier" && spec.imported.type === "Identifier" && spec.imported.name === "state") {
211
- const localName = spec.local.type === "Identifier" ? spec.local.name : null;
206
+ if (node.source.type === "Literal" && typeof node.source.value === "string" && node.source.value === "@wsxjs/wsx-core") {
207
+ node.specifiers.forEach((specifier) => {
208
+ if (specifier.type === "ImportSpecifier") {
209
+ if (specifier.imported.type === "Identifier" && specifier.imported.name === "state") {
210
+ const localName = specifier.local.type === "Identifier" ? specifier.local.name : null;
211
+ if (localName) {
212
+ stateImports.add(localName);
213
+ }
214
+ }
215
+ } else if (specifier.type === "ImportDefaultSpecifier") {
216
+ const localName = specifier.local.type === "Identifier" ? specifier.local.name : null;
212
217
  if (localName) {
213
218
  stateImports.add(localName);
214
219
  }
220
+ } else if (specifier.type === "ImportNamespaceSpecifier") {
221
+ const localName = specifier.local.type === "Identifier" ? specifier.local.name : null;
222
+ if (localName) {
223
+ stateImports.add("state");
224
+ }
215
225
  }
216
226
  });
217
227
  }
218
228
  },
229
+ // Check class properties for @state decorator
230
+ // Support both ClassProperty (older) and PropertyDefinition (newer TypeScript ESLint)
219
231
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
220
- ClassDeclaration(node) {
221
- const observedAttributes = [];
222
- for (const member of node.body.body) {
223
- if ((member.type === "ClassProperty" || member.type === "ClassPrivateProperty") && member.static && member.key.type === "Identifier" && member.key.name === "observedAttributes") {
224
- if (member.value?.type === "ArrayExpression") {
225
- observedAttributes.push(
226
- ...member.value.elements.filter((el) => el && el.type === "StringLiteral").map((el) => el.value)
227
- );
232
+ "ClassProperty, ClassPrivateProperty, PropertyDefinition"(node) {
233
+ if (!node.decorators || node.decorators.length === 0) {
234
+ return;
235
+ }
236
+ const hasStateDecorator = node.decorators.some((decorator) => {
237
+ if (decorator.expression.type === "Identifier") {
238
+ return decorator.expression.name === "state" || stateImports.has(decorator.expression.name);
239
+ } else if (decorator.expression.type === "CallExpression") {
240
+ if (decorator.expression.callee.type === "Identifier") {
241
+ return decorator.expression.callee.name === "state" || stateImports.has(decorator.expression.callee.name);
228
242
  }
229
- } else if (member.type === "ClassMethod" && member.static && member.kind === "get" && member.key.type === "Identifier" && member.key.name === "observedAttributes") {
230
- const returnStmt = member.body.body.find(
231
- (stmt) => stmt.type === "ReturnStatement"
232
- );
233
- if (returnStmt?.argument?.type === "ArrayExpression") {
234
- observedAttributes.push(
235
- ...returnStmt.argument.elements.filter((el) => el && el.type === "StringLiteral").map((el) => el.value)
236
- );
243
+ } else if (decorator.expression.type === "MemberExpression") {
244
+ if (decorator.expression.property.type === "Identifier" && decorator.expression.property.name === "state") {
245
+ return true;
237
246
  }
238
247
  }
239
- }
240
- if (observedAttributes.length > 0) {
241
- classObservedAttributes.set(node, observedAttributes);
242
- }
243
- },
244
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
245
- "ClassDeclaration:exit"(node) {
246
- classObservedAttributes.delete(node);
247
- },
248
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
249
- Decorator(node) {
250
- let isStateDecorator = false;
251
- if (node.expression.type === "Identifier") {
252
- const name = node.expression.name;
253
- isStateDecorator = name === "state" || stateImports.has(name);
254
- } else if (node.expression.type === "CallExpression") {
255
- const callee = node.expression.callee;
256
- if (callee.type === "Identifier") {
257
- const name = callee.name;
258
- isStateDecorator = name === "state" || stateImports.has(name);
259
- }
260
- }
261
- if (!isStateDecorator) {
262
- return;
263
- }
264
- let classNode = node.parent;
265
- while (classNode && classNode.type !== "ClassDeclaration" && classNode.type !== "ClassExpression") {
266
- classNode = classNode.parent;
267
- }
268
- if (!classNode) {
269
- return;
270
- }
271
- const propertyNode = node.parent;
272
- if (!propertyNode || propertyNode.type !== "ClassProperty" && propertyNode.type !== "ClassPrivateProperty") {
273
- return;
274
- }
275
- const propertyName = propertyNode.key.type === "Identifier" ? propertyNode.key.name : null;
276
- if (!propertyName) {
277
- return;
278
- }
279
- const observedAttributes = classObservedAttributes.get(classNode);
280
- if (!observedAttributes || observedAttributes.length === 0) {
281
- return;
282
- }
283
- const propertyKebabCase = propertyName.replace(/([A-Z])/g, "-$1").toLowerCase();
284
- const propertyLower = propertyName.toLowerCase();
285
- if (observedAttributes.some(
286
- (attr) => attr.toLowerCase() === propertyLower || attr.toLowerCase() === propertyKebabCase
287
- )) {
248
+ return false;
249
+ });
250
+ if (hasStateDecorator && !node.value) {
251
+ const propertyName = node.key.type === "Identifier" ? node.key.name : node.key.type === "PrivateIdentifier" ? node.key.name : "unknown";
288
252
  context.report({
289
253
  node,
290
- messageId: "stateOnHtmlAttribute",
254
+ messageId: "missingInitialValue",
291
255
  data: { propertyName }
292
256
  });
293
257
  }
@@ -296,69 +260,6 @@ var noStateOnHtmlAttributes = {
296
260
  }
297
261
  };
298
262
 
299
- // src/rules/no-state-on-methods.ts
300
- var noStateOnMethods = {
301
- meta: {
302
- type: "problem",
303
- docs: {
304
- description: "disallow @state decorator on methods",
305
- category: "Possible Errors",
306
- recommended: true
307
- },
308
- messages: {
309
- stateOnMethod: "@state decorator cannot be used on methods. '{{methodName}}' is a method, not a property. @state can only be used on properties with primitive, object, or array values."
310
- },
311
- schema: []
312
- },
313
- create(context) {
314
- const stateImports = /* @__PURE__ */ new Set();
315
- return {
316
- // Track imports
317
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
318
- ImportDeclaration(node) {
319
- if (node.source.type === "StringLiteral" && (node.source.value === "@wsxjs/wsx-core" || node.source.value.endsWith("/wsx-core"))) {
320
- node.specifiers.forEach((spec) => {
321
- if (spec.type === "ImportSpecifier" && spec.imported.type === "Identifier" && spec.imported.name === "state") {
322
- const localName = spec.local.type === "Identifier" ? spec.local.name : null;
323
- if (localName) {
324
- stateImports.add(localName);
325
- }
326
- }
327
- });
328
- }
329
- },
330
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
331
- Decorator(node) {
332
- let isStateDecorator = false;
333
- if (node.expression.type === "Identifier") {
334
- const name = node.expression.name;
335
- isStateDecorator = name === "state" || stateImports.has(name);
336
- } else if (node.expression.type === "CallExpression") {
337
- const callee = node.expression.callee;
338
- if (callee.type === "Identifier") {
339
- const name = callee.name;
340
- isStateDecorator = name === "state" || stateImports.has(name);
341
- }
342
- }
343
- if (!isStateDecorator) {
344
- return;
345
- }
346
- const parent = node.parent;
347
- if (parent && (parent.type === "ClassMethod" || parent.type === "ClassPrivateMethod")) {
348
- const methodName = parent.key.type === "Identifier" ? parent.key.name : parent.key.type === "StringLiteral" ? parent.key.value : null;
349
- if (methodName) {
350
- context.report({
351
- node,
352
- messageId: "stateOnMethod",
353
- data: { methodName }
354
- });
355
- }
356
- }
357
- }
358
- };
359
- }
360
- };
361
-
362
263
  // src/configs/recommended.ts
363
264
  var recommendedConfig = {
364
265
  parser: "@typescript-eslint/parser",
@@ -369,7 +270,9 @@ var recommendedConfig = {
369
270
  jsx: true
370
271
  },
371
272
  jsxPragma: "h",
372
- jsxFragmentName: "Fragment"
273
+ jsxFragmentName: "Fragment",
274
+ experimentalDecorators: true
275
+ // Required to parse @state decorators
373
276
  },
374
277
  plugins: ["wsx"],
375
278
  rules: {
@@ -377,8 +280,7 @@ var recommendedConfig = {
377
280
  "wsx/render-method-required": "error",
378
281
  "wsx/no-react-imports": "error",
379
282
  "wsx/web-component-naming": "warn",
380
- "wsx/no-state-on-html-attributes": "error",
381
- "wsx/no-state-on-methods": "error",
283
+ "wsx/state-requires-initial-value": "error",
382
284
  // TypeScript 规则(推荐)
383
285
  "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
384
286
  "@typescript-eslint/no-explicit-any": "warn",
@@ -544,8 +446,7 @@ var plugin = {
544
446
  "render-method-required": renderMethodRequired,
545
447
  "no-react-imports": noReactImports,
546
448
  "web-component-naming": webComponentNaming,
547
- "no-state-on-html-attributes": noStateOnHtmlAttributes,
548
- "no-state-on-methods": noStateOnMethods
449
+ "state-requires-initial-value": stateRequiresInitialValue
549
450
  },
550
451
  // 配置预设
551
452
  configs: {
package/dist/index.mjs CHANGED
@@ -155,110 +155,74 @@ var webComponentNaming = {
155
155
  }
156
156
  };
157
157
 
158
- // src/rules/no-state-on-html-attributes.ts
159
- var noStateOnHtmlAttributes = {
158
+ // src/rules/state-requires-initial-value.ts
159
+ var stateRequiresInitialValue = {
160
160
  meta: {
161
161
  type: "problem",
162
162
  docs: {
163
- description: "disallow @state decorator on HTML attributes",
163
+ description: "require @state decorator properties to have initial values",
164
164
  category: "Possible Errors",
165
165
  recommended: true
166
166
  },
167
167
  messages: {
168
- stateOnHtmlAttribute: "@state decorator cannot be used on properties that are HTML attributes. Property '{{propertyName}}' is defined in observedAttributes. HTML attributes should be handled via onAttributeChanged, not @state decorator."
168
+ missingInitialValue: "@state decorator on property '{{propertyName}}' requires an initial value.\n\nExamples:\n @state private {{propertyName}} = ''; // for string\n @state private {{propertyName}} = 0; // for number\n @state private {{propertyName}} = {}; // for object\n @state private {{propertyName}} = []; // for array\n @state private {{propertyName}} = undefined; // for optional"
169
169
  },
170
170
  schema: []
171
171
  },
172
172
  create(context) {
173
- const classObservedAttributes = /* @__PURE__ */ new Map();
174
173
  const stateImports = /* @__PURE__ */ new Set();
175
174
  return {
176
- // Track imports
177
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
175
+ // Track imports to identify @state decorator
178
176
  ImportDeclaration(node) {
179
- if (node.source.type === "StringLiteral" && (node.source.value === "@wsxjs/wsx-core" || node.source.value.endsWith("/wsx-core"))) {
180
- node.specifiers.forEach((spec) => {
181
- if (spec.type === "ImportSpecifier" && spec.imported.type === "Identifier" && spec.imported.name === "state") {
182
- const localName = spec.local.type === "Identifier" ? spec.local.name : null;
177
+ if (node.source.type === "Literal" && typeof node.source.value === "string" && node.source.value === "@wsxjs/wsx-core") {
178
+ node.specifiers.forEach((specifier) => {
179
+ if (specifier.type === "ImportSpecifier") {
180
+ if (specifier.imported.type === "Identifier" && specifier.imported.name === "state") {
181
+ const localName = specifier.local.type === "Identifier" ? specifier.local.name : null;
182
+ if (localName) {
183
+ stateImports.add(localName);
184
+ }
185
+ }
186
+ } else if (specifier.type === "ImportDefaultSpecifier") {
187
+ const localName = specifier.local.type === "Identifier" ? specifier.local.name : null;
183
188
  if (localName) {
184
189
  stateImports.add(localName);
185
190
  }
191
+ } else if (specifier.type === "ImportNamespaceSpecifier") {
192
+ const localName = specifier.local.type === "Identifier" ? specifier.local.name : null;
193
+ if (localName) {
194
+ stateImports.add("state");
195
+ }
186
196
  }
187
197
  });
188
198
  }
189
199
  },
200
+ // Check class properties for @state decorator
201
+ // Support both ClassProperty (older) and PropertyDefinition (newer TypeScript ESLint)
190
202
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
191
- ClassDeclaration(node) {
192
- const observedAttributes = [];
193
- for (const member of node.body.body) {
194
- if ((member.type === "ClassProperty" || member.type === "ClassPrivateProperty") && member.static && member.key.type === "Identifier" && member.key.name === "observedAttributes") {
195
- if (member.value?.type === "ArrayExpression") {
196
- observedAttributes.push(
197
- ...member.value.elements.filter((el) => el && el.type === "StringLiteral").map((el) => el.value)
198
- );
203
+ "ClassProperty, ClassPrivateProperty, PropertyDefinition"(node) {
204
+ if (!node.decorators || node.decorators.length === 0) {
205
+ return;
206
+ }
207
+ const hasStateDecorator = node.decorators.some((decorator) => {
208
+ if (decorator.expression.type === "Identifier") {
209
+ return decorator.expression.name === "state" || stateImports.has(decorator.expression.name);
210
+ } else if (decorator.expression.type === "CallExpression") {
211
+ if (decorator.expression.callee.type === "Identifier") {
212
+ return decorator.expression.callee.name === "state" || stateImports.has(decorator.expression.callee.name);
199
213
  }
200
- } else if (member.type === "ClassMethod" && member.static && member.kind === "get" && member.key.type === "Identifier" && member.key.name === "observedAttributes") {
201
- const returnStmt = member.body.body.find(
202
- (stmt) => stmt.type === "ReturnStatement"
203
- );
204
- if (returnStmt?.argument?.type === "ArrayExpression") {
205
- observedAttributes.push(
206
- ...returnStmt.argument.elements.filter((el) => el && el.type === "StringLiteral").map((el) => el.value)
207
- );
214
+ } else if (decorator.expression.type === "MemberExpression") {
215
+ if (decorator.expression.property.type === "Identifier" && decorator.expression.property.name === "state") {
216
+ return true;
208
217
  }
209
218
  }
210
- }
211
- if (observedAttributes.length > 0) {
212
- classObservedAttributes.set(node, observedAttributes);
213
- }
214
- },
215
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
216
- "ClassDeclaration:exit"(node) {
217
- classObservedAttributes.delete(node);
218
- },
219
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
220
- Decorator(node) {
221
- let isStateDecorator = false;
222
- if (node.expression.type === "Identifier") {
223
- const name = node.expression.name;
224
- isStateDecorator = name === "state" || stateImports.has(name);
225
- } else if (node.expression.type === "CallExpression") {
226
- const callee = node.expression.callee;
227
- if (callee.type === "Identifier") {
228
- const name = callee.name;
229
- isStateDecorator = name === "state" || stateImports.has(name);
230
- }
231
- }
232
- if (!isStateDecorator) {
233
- return;
234
- }
235
- let classNode = node.parent;
236
- while (classNode && classNode.type !== "ClassDeclaration" && classNode.type !== "ClassExpression") {
237
- classNode = classNode.parent;
238
- }
239
- if (!classNode) {
240
- return;
241
- }
242
- const propertyNode = node.parent;
243
- if (!propertyNode || propertyNode.type !== "ClassProperty" && propertyNode.type !== "ClassPrivateProperty") {
244
- return;
245
- }
246
- const propertyName = propertyNode.key.type === "Identifier" ? propertyNode.key.name : null;
247
- if (!propertyName) {
248
- return;
249
- }
250
- const observedAttributes = classObservedAttributes.get(classNode);
251
- if (!observedAttributes || observedAttributes.length === 0) {
252
- return;
253
- }
254
- const propertyKebabCase = propertyName.replace(/([A-Z])/g, "-$1").toLowerCase();
255
- const propertyLower = propertyName.toLowerCase();
256
- if (observedAttributes.some(
257
- (attr) => attr.toLowerCase() === propertyLower || attr.toLowerCase() === propertyKebabCase
258
- )) {
219
+ return false;
220
+ });
221
+ if (hasStateDecorator && !node.value) {
222
+ const propertyName = node.key.type === "Identifier" ? node.key.name : node.key.type === "PrivateIdentifier" ? node.key.name : "unknown";
259
223
  context.report({
260
224
  node,
261
- messageId: "stateOnHtmlAttribute",
225
+ messageId: "missingInitialValue",
262
226
  data: { propertyName }
263
227
  });
264
228
  }
@@ -267,69 +231,6 @@ var noStateOnHtmlAttributes = {
267
231
  }
268
232
  };
269
233
 
270
- // src/rules/no-state-on-methods.ts
271
- var noStateOnMethods = {
272
- meta: {
273
- type: "problem",
274
- docs: {
275
- description: "disallow @state decorator on methods",
276
- category: "Possible Errors",
277
- recommended: true
278
- },
279
- messages: {
280
- stateOnMethod: "@state decorator cannot be used on methods. '{{methodName}}' is a method, not a property. @state can only be used on properties with primitive, object, or array values."
281
- },
282
- schema: []
283
- },
284
- create(context) {
285
- const stateImports = /* @__PURE__ */ new Set();
286
- return {
287
- // Track imports
288
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
289
- ImportDeclaration(node) {
290
- if (node.source.type === "StringLiteral" && (node.source.value === "@wsxjs/wsx-core" || node.source.value.endsWith("/wsx-core"))) {
291
- node.specifiers.forEach((spec) => {
292
- if (spec.type === "ImportSpecifier" && spec.imported.type === "Identifier" && spec.imported.name === "state") {
293
- const localName = spec.local.type === "Identifier" ? spec.local.name : null;
294
- if (localName) {
295
- stateImports.add(localName);
296
- }
297
- }
298
- });
299
- }
300
- },
301
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
302
- Decorator(node) {
303
- let isStateDecorator = false;
304
- if (node.expression.type === "Identifier") {
305
- const name = node.expression.name;
306
- isStateDecorator = name === "state" || stateImports.has(name);
307
- } else if (node.expression.type === "CallExpression") {
308
- const callee = node.expression.callee;
309
- if (callee.type === "Identifier") {
310
- const name = callee.name;
311
- isStateDecorator = name === "state" || stateImports.has(name);
312
- }
313
- }
314
- if (!isStateDecorator) {
315
- return;
316
- }
317
- const parent = node.parent;
318
- if (parent && (parent.type === "ClassMethod" || parent.type === "ClassPrivateMethod")) {
319
- const methodName = parent.key.type === "Identifier" ? parent.key.name : parent.key.type === "StringLiteral" ? parent.key.value : null;
320
- if (methodName) {
321
- context.report({
322
- node,
323
- messageId: "stateOnMethod",
324
- data: { methodName }
325
- });
326
- }
327
- }
328
- }
329
- };
330
- }
331
- };
332
-
333
234
  // src/configs/recommended.ts
334
235
  var recommendedConfig = {
335
236
  parser: "@typescript-eslint/parser",
@@ -340,7 +241,9 @@ var recommendedConfig = {
340
241
  jsx: true
341
242
  },
342
243
  jsxPragma: "h",
343
- jsxFragmentName: "Fragment"
244
+ jsxFragmentName: "Fragment",
245
+ experimentalDecorators: true
246
+ // Required to parse @state decorators
344
247
  },
345
248
  plugins: ["wsx"],
346
249
  rules: {
@@ -348,8 +251,7 @@ var recommendedConfig = {
348
251
  "wsx/render-method-required": "error",
349
252
  "wsx/no-react-imports": "error",
350
253
  "wsx/web-component-naming": "warn",
351
- "wsx/no-state-on-html-attributes": "error",
352
- "wsx/no-state-on-methods": "error",
254
+ "wsx/state-requires-initial-value": "error",
353
255
  // TypeScript 规则(推荐)
354
256
  "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
355
257
  "@typescript-eslint/no-explicit-any": "warn",
@@ -515,8 +417,7 @@ var plugin = {
515
417
  "render-method-required": renderMethodRequired,
516
418
  "no-react-imports": noReactImports,
517
419
  "web-component-naming": webComponentNaming,
518
- "no-state-on-html-attributes": noStateOnHtmlAttributes,
519
- "no-state-on-methods": noStateOnMethods
420
+ "state-requires-initial-value": stateRequiresInitialValue
520
421
  },
521
422
  // 配置预设
522
423
  configs: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wsxjs/eslint-plugin-wsx",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "description": "ESLint plugin for WSX Framework",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -25,7 +25,7 @@
25
25
  "web-components"
26
26
  ],
27
27
  "dependencies": {
28
- "@wsxjs/wsx-core": "0.0.11"
28
+ "@wsxjs/wsx-core": "0.0.13"
29
29
  },
30
30
  "devDependencies": {
31
31
  "tsup": "^8.0.0",
@@ -16,6 +16,7 @@ export const recommendedConfig: WSXConfig = {
16
16
  },
17
17
  jsxPragma: "h",
18
18
  jsxFragmentName: "Fragment",
19
+ experimentalDecorators: true, // Required to parse @state decorators
19
20
  },
20
21
  plugins: ["wsx"],
21
22
  rules: {
@@ -23,6 +24,7 @@ export const recommendedConfig: WSXConfig = {
23
24
  "wsx/render-method-required": "error",
24
25
  "wsx/no-react-imports": "error",
25
26
  "wsx/web-component-naming": "warn",
27
+ "wsx/state-requires-initial-value": "error",
26
28
 
27
29
  // TypeScript 规则(推荐)
28
30
  "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  import { renderMethodRequired } from "./rules/render-method-required";
9
9
  import { noReactImports } from "./rules/no-react-imports";
10
10
  import { webComponentNaming } from "./rules/web-component-naming";
11
+ import { stateRequiresInitialValue } from "./rules/state-requires-initial-value";
11
12
  import { recommendedConfig } from "./configs/recommended";
12
13
  import { createFlatConfig } from "./configs/flat";
13
14
  import { WSXPlugin } from "./types";
@@ -24,6 +25,7 @@ const plugin: WSXPlugin = {
24
25
  "render-method-required": renderMethodRequired,
25
26
  "no-react-imports": noReactImports,
26
27
  "web-component-naming": webComponentNaming,
28
+ "state-requires-initial-value": stateRequiresInitialValue,
27
29
  },
28
30
 
29
31
  // 配置预设
@@ -0,0 +1,136 @@
1
+ /**
2
+ * ESLint 规则:state-requires-initial-value
3
+ *
4
+ * 确保 @state 装饰器的属性必须有初始值
5
+ * 这是强制性的,因为我们需要初始值来判断是 primitive 还是 object/array
6
+ */
7
+
8
+ import { Rule } from "eslint";
9
+ import { WSXRuleModule } from "../types";
10
+
11
+ export const stateRequiresInitialValue: WSXRuleModule = {
12
+ meta: {
13
+ type: "problem",
14
+ docs: {
15
+ description: "require @state decorator properties to have initial values",
16
+ category: "Possible Errors",
17
+ recommended: true,
18
+ },
19
+ messages: {
20
+ missingInitialValue:
21
+ "@state decorator on property '{{propertyName}}' requires an initial value.\n" +
22
+ "\n" +
23
+ "Examples:\n" +
24
+ " @state private {{propertyName}} = ''; // for string\n" +
25
+ " @state private {{propertyName}} = 0; // for number\n" +
26
+ " @state private {{propertyName}} = {}; // for object\n" +
27
+ " @state private {{propertyName}} = []; // for array\n" +
28
+ " @state private {{propertyName}} = undefined; // for optional",
29
+ },
30
+ schema: [],
31
+ },
32
+ create(context: Rule.RuleContext) {
33
+ // Track imported 'state' identifiers from @wsxjs/wsx-core
34
+ const stateImports = new Set<string>();
35
+
36
+ return {
37
+ // Track imports to identify @state decorator
38
+ ImportDeclaration(node) {
39
+ if (
40
+ node.source.type === "Literal" &&
41
+ typeof node.source.value === "string" &&
42
+ node.source.value === "@wsxjs/wsx-core"
43
+ ) {
44
+ node.specifiers.forEach((specifier) => {
45
+ if (specifier.type === "ImportSpecifier") {
46
+ if (
47
+ specifier.imported.type === "Identifier" &&
48
+ specifier.imported.name === "state"
49
+ ) {
50
+ // Track both the imported name and any alias
51
+ const localName =
52
+ specifier.local.type === "Identifier"
53
+ ? specifier.local.name
54
+ : null;
55
+ if (localName) {
56
+ stateImports.add(localName);
57
+ }
58
+ }
59
+ } else if (specifier.type === "ImportDefaultSpecifier") {
60
+ // Handle default import (less common but possible)
61
+ const localName =
62
+ specifier.local.type === "Identifier" ? specifier.local.name : null;
63
+ if (localName) {
64
+ stateImports.add(localName);
65
+ }
66
+ } else if (specifier.type === "ImportNamespaceSpecifier") {
67
+ // Handle namespace import: import * as wsx from '@wsxjs/wsx-core'
68
+ // In this case, @state would be wsx.state, which is harder to detect
69
+ // We'll check for both 'state' and namespace.state patterns
70
+ const localName =
71
+ specifier.local.type === "Identifier" ? specifier.local.name : null;
72
+ if (localName) {
73
+ // For namespace imports, we'd need to check member expressions
74
+ // This is more complex, so we'll also check for plain 'state'
75
+ stateImports.add("state");
76
+ }
77
+ }
78
+ });
79
+ }
80
+ },
81
+
82
+ // Check class properties for @state decorator
83
+ // Support both ClassProperty (older) and PropertyDefinition (newer TypeScript ESLint)
84
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
85
+ "ClassProperty, ClassPrivateProperty, PropertyDefinition"(node: any) {
86
+ if (!node.decorators || node.decorators.length === 0) {
87
+ return;
88
+ }
89
+
90
+ // Check if any decorator is @state
91
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
92
+ const hasStateDecorator = node.decorators.some((decorator: any) => {
93
+ if (decorator.expression.type === "Identifier") {
94
+ // Direct identifier: @state
95
+ return (
96
+ decorator.expression.name === "state" ||
97
+ stateImports.has(decorator.expression.name)
98
+ );
99
+ } else if (decorator.expression.type === "CallExpression") {
100
+ // Call expression: @state()
101
+ if (decorator.expression.callee.type === "Identifier") {
102
+ return (
103
+ decorator.expression.callee.name === "state" ||
104
+ stateImports.has(decorator.expression.callee.name)
105
+ );
106
+ }
107
+ } else if (decorator.expression.type === "MemberExpression") {
108
+ // Member expression: @namespace.state
109
+ if (
110
+ decorator.expression.property.type === "Identifier" &&
111
+ decorator.expression.property.name === "state"
112
+ ) {
113
+ return true;
114
+ }
115
+ }
116
+ return false;
117
+ });
118
+
119
+ if (hasStateDecorator && !node.value) {
120
+ const propertyName =
121
+ node.key.type === "Identifier"
122
+ ? node.key.name
123
+ : node.key.type === "PrivateIdentifier"
124
+ ? node.key.name
125
+ : "unknown";
126
+
127
+ context.report({
128
+ node,
129
+ messageId: "missingInitialValue",
130
+ data: { propertyName },
131
+ });
132
+ }
133
+ },
134
+ };
135
+ },
136
+ };
package/src/types.ts CHANGED
@@ -24,6 +24,7 @@ export interface WSXConfig {
24
24
  };
25
25
  jsxPragma?: string;
26
26
  jsxFragmentName?: string;
27
+ experimentalDecorators?: boolean; // Required for @state decorator support
27
28
  };
28
29
  plugins?: string[];
29
30
  rules?: Record<string, unknown>;