@wsxjs/eslint-plugin-wsx 0.0.11 → 0.0.12

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,192 @@
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
+ // WSX plugin rules
62
+ "wsx/render-method-required": "error",
63
+ "wsx/no-react-imports": "error",
64
+ "wsx/web-component-naming": "warn",
65
+ "wsx/state-requires-initial-value": "error",
66
+ },
67
+ },
68
+ ];
69
+ ```
70
+
71
+ ### Required Dependencies
72
+
73
+ Make sure you have these peer dependencies installed:
74
+
75
+ ```bash
76
+ npm install --save-dev eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser globals
77
+ ```
78
+
79
+ ## Rules
80
+
81
+ ### `wsx/render-method-required`
82
+
83
+ **Error level**: `error`
84
+
85
+ Ensures WSX components implement the required `render()` method.
86
+
87
+ **Invalid**:
88
+ ```typescript
89
+ class MyComponent extends WebComponent {
90
+ // Missing render() method
91
+ }
92
+ ```
93
+
94
+ **Valid**:
95
+ ```typescript
96
+ class MyComponent extends WebComponent {
97
+ render() {
98
+ return <div>Hello</div>;
99
+ }
100
+ }
101
+ ```
102
+
103
+ ### `wsx/no-react-imports`
104
+
105
+ **Error level**: `error`
106
+
107
+ Prevents React imports in WSX files. WSX uses its own JSX runtime.
108
+
109
+ **Invalid**:
110
+ ```typescript
111
+ import React from "react"; // ❌
112
+ import { useState } from "react"; // ❌
113
+ ```
114
+
115
+ **Valid**:
116
+ ```typescript
117
+ import { WebComponent, state } from "@wsxjs/wsx-core"; // ✅
118
+ ```
119
+
120
+ ### `wsx/web-component-naming`
121
+
122
+ **Error level**: `warn`
123
+
124
+ Enforces proper Web Component tag naming conventions (kebab-case with at least one hyphen).
125
+
126
+ **Invalid**:
127
+ ```typescript
128
+ @autoRegister({ tagName: "mycomponent" }) // ❌ Missing hyphen
129
+ @autoRegister({ tagName: "MyComponent" }) // ❌ Not kebab-case
130
+ ```
131
+
132
+ **Valid**:
133
+ ```typescript
134
+ @autoRegister({ tagName: "my-component" }) // ✅
135
+ @autoRegister({ tagName: "wsx-button" }) // ✅
136
+ ```
137
+
138
+ ### `wsx/state-requires-initial-value`
139
+
140
+ **Error level**: `error`
141
+
142
+ Requires `@state` decorator properties to have initial values. This is mandatory because we need the initial value to determine if it's a primitive or object/array.
143
+
144
+ **Invalid**:
145
+ ```typescript
146
+ class MyComponent extends WebComponent {
147
+ @state private maskStrokeColor?: string; // ❌ Missing initial value
148
+ }
149
+ ```
150
+
151
+ **Valid**:
152
+ ```typescript
153
+ class MyComponent extends WebComponent {
154
+ @state private maskStrokeColor = ""; // ✅ String
155
+ @state private count = 0; // ✅ Number
156
+ @state private user = { name: "John" }; // ✅ Object
157
+ @state private items = []; // ✅ Array
158
+ @state private optional?: string = undefined; // ✅ Optional with explicit undefined
159
+ }
160
+ ```
161
+
162
+ ## Configuration Options
163
+
164
+ ### Disable Specific Rules
165
+
166
+ If you need to disable a specific rule:
167
+
168
+ ```javascript
169
+ {
170
+ rules: {
171
+ "wsx/web-component-naming": "off", // Disable naming rule
172
+ "wsx/state-requires-initial-value": "warn", // Change to warning
173
+ },
174
+ }
175
+ ```
176
+
177
+ ### File-Specific Rules
178
+
179
+ Apply rules only to `.wsx` files:
180
+
181
+ ```javascript
182
+ {
183
+ files: ["**/*.wsx"],
184
+ rules: {
185
+ "wsx/render-method-required": "error",
186
+ "wsx/no-react-imports": "error",
187
+ },
188
+ }
189
+ ```
190
+
5
191
  ## Testing Results
6
192
 
7
193
  ✅ **38 tests passed** with **100% code coverage**
@@ -38,6 +224,7 @@ This plugin now uses industry-standard testing practices:
38
224
  - 🔍 **render-method-required**: Ensures WSX components implement the required `render()` method
39
225
  - 🚫 **no-react-imports**: Prevents React imports in WSX files
40
226
  - 🏷️ **web-component-naming**: Enforces proper Web Component tag naming conventions
227
+ - ✅ **state-requires-initial-value**: Requires `@state` decorator properties to have initial values
41
228
 
42
229
  ## Framework Integration
43
230
 
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.12",
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.12"
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>;