@wsxjs/eslint-plugin-wsx 0.0.17 → 0.0.18

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 WSX Framework Contributors
3
+ Copyright (c) 2026 WSXJS Contributors
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
18
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @wsxjs/eslint-plugin-wsx
2
2
 
3
- ESLint plugin for WSX Framework - enforces best practices and framework-specific rules for Web Components with JSX.
3
+ ESLint plugin for WSXJS - enforces best practices and framework-specific rules for Web Components with JSX.
4
4
 
5
5
  ## Installation
6
6
 
package/dist/index.d.mts CHANGED
@@ -10,6 +10,7 @@ interface WSXRuleContext extends Rule.RuleContext {
10
10
  }
11
11
  interface WSXRuleModule extends Rule.RuleModule {
12
12
  create: (context: WSXRuleContext) => Rule.RuleListener;
13
+ defaultOptions?: unknown[];
13
14
  }
14
15
  interface WSXConfig {
15
16
  parser?: string;
package/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ interface WSXRuleContext extends Rule.RuleContext {
10
10
  }
11
11
  interface WSXRuleModule extends Rule.RuleModule {
12
12
  create: (context: WSXRuleContext) => Rule.RuleListener;
13
+ defaultOptions?: unknown[];
13
14
  }
14
15
  interface WSXConfig {
15
16
  parser?: string;
package/dist/index.js CHANGED
@@ -260,6 +260,196 @@ var stateRequiresInitialValue = {
260
260
  }
261
261
  };
262
262
 
263
+ // src/rules/require-jsx-import-source.ts
264
+ var JSX_IMPORT_SOURCE_PRAGMA = "/** @jsxImportSource @wsxjs/wsx-core */";
265
+ var requireJsxImportSource = {
266
+ meta: {
267
+ type: "problem",
268
+ docs: {
269
+ description: "require @jsxImportSource pragma in .wsx files",
270
+ category: "Best Practices",
271
+ recommended: true
272
+ },
273
+ fixable: "code",
274
+ messages: {
275
+ missingPragma: "WSX files must include '@jsxImportSource @wsxjs/wsx-core' pragma comment at the top for IDE TypeScript language server support"
276
+ },
277
+ schema: []
278
+ // 无配置选项
279
+ },
280
+ defaultOptions: [],
281
+ // 无默认配置选项
282
+ create(context) {
283
+ const filename = context.getFilename();
284
+ if (!filename.endsWith(".wsx")) {
285
+ return {};
286
+ }
287
+ const sourceCode = context.getSourceCode();
288
+ const text = sourceCode.getText();
289
+ const hasPragma = text.includes("@jsxImportSource");
290
+ if (!hasPragma) {
291
+ const firstNode = sourceCode.ast.body[0] || sourceCode.ast;
292
+ context.report({
293
+ node: firstNode,
294
+ messageId: "missingPragma",
295
+ fix(fixer) {
296
+ return fixer.insertTextBeforeRange([0, 0], `${JSX_IMPORT_SOURCE_PRAGMA}
297
+ `);
298
+ }
299
+ });
300
+ }
301
+ return {};
302
+ }
303
+ };
304
+
305
+ // src/rules/no-null-render.ts
306
+ var noNullRender = {
307
+ meta: {
308
+ type: "problem",
309
+ docs: {
310
+ description: "disallow returning null from render() method",
311
+ category: "Possible Errors",
312
+ recommended: true
313
+ },
314
+ fixable: "code",
315
+ messages: {
316
+ nullReturn: "render() method must not return null. WebComponent and LightComponent require a valid DOM node. Use an empty div as placeholder instead."
317
+ },
318
+ schema: []
319
+ // 无配置选项
320
+ },
321
+ defaultOptions: [],
322
+ create(context) {
323
+ function checkStatements(statements) {
324
+ statements.forEach((statement) => {
325
+ if (statement.type === "ReturnStatement" && statement.argument) {
326
+ const arg = statement.argument;
327
+ if (arg.type === "Literal" && (arg.value === null || arg.raw === "null")) {
328
+ context.report({
329
+ node: statement,
330
+ messageId: "nullReturn",
331
+ fix(fixer) {
332
+ const sourceCode = context.getSourceCode();
333
+ const returnToken = sourceCode.getFirstToken(statement);
334
+ const nullToken = sourceCode.getTokenAfter(returnToken);
335
+ const semicolonToken = sourceCode.getTokenAfter(nullToken);
336
+ if (!returnToken || !nullToken) return null;
337
+ const start = returnToken.range[0];
338
+ const end = semicolonToken ? semicolonToken.range[1] : nullToken.range[1];
339
+ return fixer.replaceTextRange([start, end], "return <div></div>;");
340
+ }
341
+ });
342
+ }
343
+ }
344
+ if (statement.type === "IfStatement") {
345
+ if (statement.consequent.type === "BlockStatement") {
346
+ checkStatements(statement.consequent.body);
347
+ }
348
+ if (statement.alternate) {
349
+ if (statement.alternate.type === "BlockStatement") {
350
+ checkStatements(statement.alternate.body);
351
+ } else if (statement.alternate.type === "IfStatement") {
352
+ checkStatements([statement.alternate]);
353
+ }
354
+ }
355
+ } else if (statement.type === "SwitchStatement") {
356
+ statement.cases.forEach((caseClause) => {
357
+ checkStatements(caseClause.consequent);
358
+ });
359
+ } else if (statement.type === "ForStatement" || statement.type === "WhileStatement" || statement.type === "DoWhileStatement") {
360
+ if (statement.body.type === "BlockStatement") {
361
+ checkStatements(statement.body.body);
362
+ }
363
+ } else if (statement.type === "ForInStatement" || statement.type === "ForOfStatement") {
364
+ if (statement.body.type === "BlockStatement") {
365
+ checkStatements(statement.body.body);
366
+ }
367
+ } else if (statement.type === "TryStatement") {
368
+ if (statement.block.type === "BlockStatement") {
369
+ checkStatements(statement.block.body);
370
+ }
371
+ if (statement.handler && statement.handler.body.type === "BlockStatement") {
372
+ checkStatements(statement.handler.body.body);
373
+ }
374
+ if (statement.finalizer && statement.finalizer.type === "BlockStatement") {
375
+ checkStatements(statement.finalizer.body);
376
+ }
377
+ }
378
+ });
379
+ }
380
+ function checkReturnNull(node) {
381
+ const isRenderMethod = node.key.type === "Identifier" && node.key.name === "render" || node.key.type === "Literal" && node.key.value === "render";
382
+ if (!isRenderMethod) return;
383
+ const methodValue = node.type === "MethodDefinition" ? node.value : node.value;
384
+ if (!methodValue || methodValue.type !== "FunctionExpression") return;
385
+ const body = methodValue.body;
386
+ if (!body || body.type !== "BlockStatement") return;
387
+ checkStatements(body.body);
388
+ }
389
+ return {
390
+ // 检查类方法定义
391
+ MethodDefinition(node) {
392
+ const ancestors = context.getAncestors();
393
+ let classDeclaration = null;
394
+ for (let i = ancestors.length - 1; i >= 0; i--) {
395
+ const ancestor = ancestors[i];
396
+ if (ancestor && ancestor.type === "ClassDeclaration") {
397
+ classDeclaration = ancestor;
398
+ break;
399
+ }
400
+ }
401
+ if (classDeclaration && classDeclaration.superClass) {
402
+ const isWSXComponent = classDeclaration.superClass.type === "Identifier" && (classDeclaration.superClass.name === "WebComponent" || classDeclaration.superClass.name === "LightComponent");
403
+ if (isWSXComponent) {
404
+ checkReturnNull(node);
405
+ }
406
+ }
407
+ }
408
+ };
409
+ }
410
+ };
411
+
412
+ // src/rules/no-inner-html.ts
413
+ var noInnerHTML = {
414
+ meta: {
415
+ type: "problem",
416
+ docs: {
417
+ description: "disallow use of innerHTML in WSX code",
418
+ category: "Best Practices",
419
+ recommended: true
420
+ },
421
+ messages: {
422
+ noInnerHTML: "Do not use innerHTML. Use JSX and WSX's declarative rendering instead. If you need to parse HTML strings, use parseHTMLToNodes() utility function."
423
+ },
424
+ schema: []
425
+ // 无配置选项
426
+ },
427
+ create(context) {
428
+ return {
429
+ // 检测 innerHTML 赋值:element.innerHTML = "..."
430
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
431
+ AssignmentExpression(node) {
432
+ if (node.left && node.left.type === "MemberExpression" && node.left.property && node.left.property.type === "Identifier" && node.left.property.name === "innerHTML") {
433
+ context.report({
434
+ node,
435
+ messageId: "noInnerHTML"
436
+ });
437
+ }
438
+ },
439
+ // 检测 innerHTML 读取:const html = element.innerHTML
440
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
441
+ MemberExpression(node) {
442
+ if (node.property && node.property.type === "Identifier" && node.property.name === "innerHTML" && node.parent && node.parent.type !== "AssignmentExpression") {
443
+ context.report({
444
+ node,
445
+ messageId: "noInnerHTML"
446
+ });
447
+ }
448
+ }
449
+ };
450
+ }
451
+ };
452
+
263
453
  // src/configs/recommended.ts
264
454
  var recommendedConfig = {
265
455
  parser: "@typescript-eslint/parser",
@@ -281,6 +471,9 @@ var recommendedConfig = {
281
471
  "wsx/no-react-imports": "error",
282
472
  "wsx/web-component-naming": "warn",
283
473
  "wsx/state-requires-initial-value": "error",
474
+ "wsx/require-jsx-import-source": "error",
475
+ "wsx/no-null-render": "error",
476
+ "wsx/no-inner-html": "error",
284
477
  // TypeScript 规则(推荐)
285
478
  "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
286
479
  "@typescript-eslint/no-explicit-any": "warn",
@@ -446,7 +639,10 @@ var plugin = {
446
639
  "render-method-required": renderMethodRequired,
447
640
  "no-react-imports": noReactImports,
448
641
  "web-component-naming": webComponentNaming,
449
- "state-requires-initial-value": stateRequiresInitialValue
642
+ "state-requires-initial-value": stateRequiresInitialValue,
643
+ "require-jsx-import-source": requireJsxImportSource,
644
+ "no-null-render": noNullRender,
645
+ "no-inner-html": noInnerHTML
450
646
  },
451
647
  // 配置预设
452
648
  configs: {
package/dist/index.mjs CHANGED
@@ -231,6 +231,196 @@ var stateRequiresInitialValue = {
231
231
  }
232
232
  };
233
233
 
234
+ // src/rules/require-jsx-import-source.ts
235
+ var JSX_IMPORT_SOURCE_PRAGMA = "/** @jsxImportSource @wsxjs/wsx-core */";
236
+ var requireJsxImportSource = {
237
+ meta: {
238
+ type: "problem",
239
+ docs: {
240
+ description: "require @jsxImportSource pragma in .wsx files",
241
+ category: "Best Practices",
242
+ recommended: true
243
+ },
244
+ fixable: "code",
245
+ messages: {
246
+ missingPragma: "WSX files must include '@jsxImportSource @wsxjs/wsx-core' pragma comment at the top for IDE TypeScript language server support"
247
+ },
248
+ schema: []
249
+ // 无配置选项
250
+ },
251
+ defaultOptions: [],
252
+ // 无默认配置选项
253
+ create(context) {
254
+ const filename = context.getFilename();
255
+ if (!filename.endsWith(".wsx")) {
256
+ return {};
257
+ }
258
+ const sourceCode = context.getSourceCode();
259
+ const text = sourceCode.getText();
260
+ const hasPragma = text.includes("@jsxImportSource");
261
+ if (!hasPragma) {
262
+ const firstNode = sourceCode.ast.body[0] || sourceCode.ast;
263
+ context.report({
264
+ node: firstNode,
265
+ messageId: "missingPragma",
266
+ fix(fixer) {
267
+ return fixer.insertTextBeforeRange([0, 0], `${JSX_IMPORT_SOURCE_PRAGMA}
268
+ `);
269
+ }
270
+ });
271
+ }
272
+ return {};
273
+ }
274
+ };
275
+
276
+ // src/rules/no-null-render.ts
277
+ var noNullRender = {
278
+ meta: {
279
+ type: "problem",
280
+ docs: {
281
+ description: "disallow returning null from render() method",
282
+ category: "Possible Errors",
283
+ recommended: true
284
+ },
285
+ fixable: "code",
286
+ messages: {
287
+ nullReturn: "render() method must not return null. WebComponent and LightComponent require a valid DOM node. Use an empty div as placeholder instead."
288
+ },
289
+ schema: []
290
+ // 无配置选项
291
+ },
292
+ defaultOptions: [],
293
+ create(context) {
294
+ function checkStatements(statements) {
295
+ statements.forEach((statement) => {
296
+ if (statement.type === "ReturnStatement" && statement.argument) {
297
+ const arg = statement.argument;
298
+ if (arg.type === "Literal" && (arg.value === null || arg.raw === "null")) {
299
+ context.report({
300
+ node: statement,
301
+ messageId: "nullReturn",
302
+ fix(fixer) {
303
+ const sourceCode = context.getSourceCode();
304
+ const returnToken = sourceCode.getFirstToken(statement);
305
+ const nullToken = sourceCode.getTokenAfter(returnToken);
306
+ const semicolonToken = sourceCode.getTokenAfter(nullToken);
307
+ if (!returnToken || !nullToken) return null;
308
+ const start = returnToken.range[0];
309
+ const end = semicolonToken ? semicolonToken.range[1] : nullToken.range[1];
310
+ return fixer.replaceTextRange([start, end], "return <div></div>;");
311
+ }
312
+ });
313
+ }
314
+ }
315
+ if (statement.type === "IfStatement") {
316
+ if (statement.consequent.type === "BlockStatement") {
317
+ checkStatements(statement.consequent.body);
318
+ }
319
+ if (statement.alternate) {
320
+ if (statement.alternate.type === "BlockStatement") {
321
+ checkStatements(statement.alternate.body);
322
+ } else if (statement.alternate.type === "IfStatement") {
323
+ checkStatements([statement.alternate]);
324
+ }
325
+ }
326
+ } else if (statement.type === "SwitchStatement") {
327
+ statement.cases.forEach((caseClause) => {
328
+ checkStatements(caseClause.consequent);
329
+ });
330
+ } else if (statement.type === "ForStatement" || statement.type === "WhileStatement" || statement.type === "DoWhileStatement") {
331
+ if (statement.body.type === "BlockStatement") {
332
+ checkStatements(statement.body.body);
333
+ }
334
+ } else if (statement.type === "ForInStatement" || statement.type === "ForOfStatement") {
335
+ if (statement.body.type === "BlockStatement") {
336
+ checkStatements(statement.body.body);
337
+ }
338
+ } else if (statement.type === "TryStatement") {
339
+ if (statement.block.type === "BlockStatement") {
340
+ checkStatements(statement.block.body);
341
+ }
342
+ if (statement.handler && statement.handler.body.type === "BlockStatement") {
343
+ checkStatements(statement.handler.body.body);
344
+ }
345
+ if (statement.finalizer && statement.finalizer.type === "BlockStatement") {
346
+ checkStatements(statement.finalizer.body);
347
+ }
348
+ }
349
+ });
350
+ }
351
+ function checkReturnNull(node) {
352
+ const isRenderMethod = node.key.type === "Identifier" && node.key.name === "render" || node.key.type === "Literal" && node.key.value === "render";
353
+ if (!isRenderMethod) return;
354
+ const methodValue = node.type === "MethodDefinition" ? node.value : node.value;
355
+ if (!methodValue || methodValue.type !== "FunctionExpression") return;
356
+ const body = methodValue.body;
357
+ if (!body || body.type !== "BlockStatement") return;
358
+ checkStatements(body.body);
359
+ }
360
+ return {
361
+ // 检查类方法定义
362
+ MethodDefinition(node) {
363
+ const ancestors = context.getAncestors();
364
+ let classDeclaration = null;
365
+ for (let i = ancestors.length - 1; i >= 0; i--) {
366
+ const ancestor = ancestors[i];
367
+ if (ancestor && ancestor.type === "ClassDeclaration") {
368
+ classDeclaration = ancestor;
369
+ break;
370
+ }
371
+ }
372
+ if (classDeclaration && classDeclaration.superClass) {
373
+ const isWSXComponent = classDeclaration.superClass.type === "Identifier" && (classDeclaration.superClass.name === "WebComponent" || classDeclaration.superClass.name === "LightComponent");
374
+ if (isWSXComponent) {
375
+ checkReturnNull(node);
376
+ }
377
+ }
378
+ }
379
+ };
380
+ }
381
+ };
382
+
383
+ // src/rules/no-inner-html.ts
384
+ var noInnerHTML = {
385
+ meta: {
386
+ type: "problem",
387
+ docs: {
388
+ description: "disallow use of innerHTML in WSX code",
389
+ category: "Best Practices",
390
+ recommended: true
391
+ },
392
+ messages: {
393
+ noInnerHTML: "Do not use innerHTML. Use JSX and WSX's declarative rendering instead. If you need to parse HTML strings, use parseHTMLToNodes() utility function."
394
+ },
395
+ schema: []
396
+ // 无配置选项
397
+ },
398
+ create(context) {
399
+ return {
400
+ // 检测 innerHTML 赋值:element.innerHTML = "..."
401
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
402
+ AssignmentExpression(node) {
403
+ if (node.left && node.left.type === "MemberExpression" && node.left.property && node.left.property.type === "Identifier" && node.left.property.name === "innerHTML") {
404
+ context.report({
405
+ node,
406
+ messageId: "noInnerHTML"
407
+ });
408
+ }
409
+ },
410
+ // 检测 innerHTML 读取:const html = element.innerHTML
411
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
412
+ MemberExpression(node) {
413
+ if (node.property && node.property.type === "Identifier" && node.property.name === "innerHTML" && node.parent && node.parent.type !== "AssignmentExpression") {
414
+ context.report({
415
+ node,
416
+ messageId: "noInnerHTML"
417
+ });
418
+ }
419
+ }
420
+ };
421
+ }
422
+ };
423
+
234
424
  // src/configs/recommended.ts
235
425
  var recommendedConfig = {
236
426
  parser: "@typescript-eslint/parser",
@@ -252,6 +442,9 @@ var recommendedConfig = {
252
442
  "wsx/no-react-imports": "error",
253
443
  "wsx/web-component-naming": "warn",
254
444
  "wsx/state-requires-initial-value": "error",
445
+ "wsx/require-jsx-import-source": "error",
446
+ "wsx/no-null-render": "error",
447
+ "wsx/no-inner-html": "error",
255
448
  // TypeScript 规则(推荐)
256
449
  "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
257
450
  "@typescript-eslint/no-explicit-any": "warn",
@@ -417,7 +610,10 @@ var plugin = {
417
610
  "render-method-required": renderMethodRequired,
418
611
  "no-react-imports": noReactImports,
419
612
  "web-component-naming": webComponentNaming,
420
- "state-requires-initial-value": stateRequiresInitialValue
613
+ "state-requires-initial-value": stateRequiresInitialValue,
614
+ "require-jsx-import-source": requireJsxImportSource,
615
+ "no-null-render": noNullRender,
616
+ "no-inner-html": noInnerHTML
421
617
  },
422
618
  // 配置预设
423
619
  configs: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@wsxjs/eslint-plugin-wsx",
3
- "version": "0.0.17",
4
- "description": "ESLint plugin for WSX Framework",
3
+ "version": "0.0.18",
4
+ "description": "ESLint plugin for WSXJS",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
7
7
  "types": "./dist/index.d.ts",
@@ -25,7 +25,7 @@
25
25
  "web-components"
26
26
  ],
27
27
  "dependencies": {
28
- "@wsxjs/wsx-core": "0.0.17"
28
+ "@wsxjs/wsx-core": "0.0.18"
29
29
  },
30
30
  "devDependencies": {
31
31
  "tsup": "^8.0.0",
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * ESLint Plugin WSX - Flat Config for ESLint 9+
3
3
  *
4
- * Modern flat config format for WSX framework
4
+ * Modern flat config format for WSXJS
5
5
  */
6
6
 
7
7
  import type { Linter } from "eslint";
@@ -25,6 +25,9 @@ export const recommendedConfig: WSXConfig = {
25
25
  "wsx/no-react-imports": "error",
26
26
  "wsx/web-component-naming": "warn",
27
27
  "wsx/state-requires-initial-value": "error",
28
+ "wsx/require-jsx-import-source": "error",
29
+ "wsx/no-null-render": "error",
30
+ "wsx/no-inner-html": "error",
28
31
 
29
32
  // TypeScript 规则(推荐)
30
33
  "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
package/src/index.ts CHANGED
@@ -9,6 +9,9 @@ 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
11
  import { stateRequiresInitialValue } from "./rules/state-requires-initial-value";
12
+ import { requireJsxImportSource } from "./rules/require-jsx-import-source";
13
+ import { noNullRender } from "./rules/no-null-render";
14
+ import { noInnerHTML } from "./rules/no-inner-html";
12
15
  import { recommendedConfig } from "./configs/recommended";
13
16
  import { createFlatConfig } from "./configs/flat";
14
17
  import { WSXPlugin } from "./types";
@@ -26,6 +29,9 @@ const plugin: WSXPlugin = {
26
29
  "no-react-imports": noReactImports,
27
30
  "web-component-naming": webComponentNaming,
28
31
  "state-requires-initial-value": stateRequiresInitialValue,
32
+ "require-jsx-import-source": requireJsxImportSource,
33
+ "no-null-render": noNullRender,
34
+ "no-inner-html": noInnerHTML,
29
35
  },
30
36
 
31
37
  // 配置预设
@@ -0,0 +1,64 @@
1
+ /**
2
+ * ESLint 规则:no-inner-html
3
+ *
4
+ * 禁止在 WSX 代码中使用 innerHTML
5
+ * WSX 框架鼓励使用声明式 JSX 而不是手动 DOM 操作
6
+ */
7
+
8
+ import { Rule } from "eslint";
9
+ import { WSXRuleModule } from "../types";
10
+
11
+ export const noInnerHTML: WSXRuleModule = {
12
+ meta: {
13
+ type: "problem",
14
+ docs: {
15
+ description: "disallow use of innerHTML in WSX code",
16
+ category: "Best Practices",
17
+ recommended: true,
18
+ },
19
+ messages: {
20
+ noInnerHTML:
21
+ "Do not use innerHTML. Use JSX and WSX's declarative rendering instead. If you need to parse HTML strings, use parseHTMLToNodes() utility function.",
22
+ },
23
+ schema: [], // 无配置选项
24
+ },
25
+ create(context: Rule.RuleContext) {
26
+ return {
27
+ // 检测 innerHTML 赋值:element.innerHTML = "..."
28
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
+ AssignmentExpression(node: any) {
30
+ if (
31
+ node.left &&
32
+ node.left.type === "MemberExpression" &&
33
+ node.left.property &&
34
+ node.left.property.type === "Identifier" &&
35
+ node.left.property.name === "innerHTML"
36
+ ) {
37
+ context.report({
38
+ node,
39
+ messageId: "noInnerHTML",
40
+ });
41
+ }
42
+ },
43
+ // 检测 innerHTML 读取:const html = element.innerHTML
44
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
+ MemberExpression(node: any) {
46
+ if (
47
+ node.property &&
48
+ node.property.type === "Identifier" &&
49
+ node.property.name === "innerHTML" &&
50
+ node.parent &&
51
+ node.parent.type !== "AssignmentExpression"
52
+ ) {
53
+ // 只报告读取操作(赋值已在上面处理)
54
+ // 但允许在特定上下文中读取(如调试或工具函数)
55
+ // 这里我们仍然报告,但用户可以通过注释禁用
56
+ context.report({
57
+ node,
58
+ messageId: "noInnerHTML",
59
+ });
60
+ }
61
+ },
62
+ };
63
+ },
64
+ };
@@ -0,0 +1,171 @@
1
+ /**
2
+ * ESLint 规则:no-null-render
3
+ *
4
+ * 禁止 render() 方法返回 null
5
+ * WebComponent 和 LightComponent 的 connectedCallback 需要有效的 DOM 节点
6
+ */
7
+
8
+ import { Rule } from "eslint";
9
+ import { WSXRuleModule } from "../types";
10
+
11
+ export const noNullRender: WSXRuleModule = {
12
+ meta: {
13
+ type: "problem",
14
+ docs: {
15
+ description: "disallow returning null from render() method",
16
+ category: "Possible Errors",
17
+ recommended: true,
18
+ },
19
+ fixable: "code",
20
+ messages: {
21
+ nullReturn:
22
+ "render() method must not return null. WebComponent and LightComponent require a valid DOM node. Use an empty div as placeholder instead.",
23
+ },
24
+ schema: [], // 无配置选项
25
+ },
26
+ defaultOptions: [],
27
+ create(context: Rule.RuleContext) {
28
+ /**
29
+ * 递归检查语句块中的所有 return null
30
+ */
31
+ function checkStatements(statements: import("estree").Statement[]) {
32
+ statements.forEach((statement) => {
33
+ // 检查 return null
34
+ if (statement.type === "ReturnStatement" && statement.argument) {
35
+ const arg = statement.argument;
36
+ // 检查是否为 null 字面量
37
+ if (arg.type === "Literal" && (arg.value === null || arg.raw === "null")) {
38
+ context.report({
39
+ node: statement,
40
+ messageId: "nullReturn",
41
+ fix(fixer) {
42
+ // 将 return null; 替换为 return <div></div>;
43
+ const sourceCode = context.getSourceCode();
44
+ const returnToken = sourceCode.getFirstToken(statement);
45
+ const nullToken = sourceCode.getTokenAfter(returnToken!);
46
+ const semicolonToken = sourceCode.getTokenAfter(nullToken!);
47
+
48
+ if (!returnToken || !nullToken) return null;
49
+
50
+ // 计算替换范围:从 return 到 null(或分号)
51
+ const start = returnToken.range[0];
52
+ const end = semicolonToken
53
+ ? semicolonToken.range[1]
54
+ : nullToken.range[1];
55
+
56
+ // 替换为 return <div></div>;
57
+ return fixer.replaceTextRange([start, end], "return <div></div>;");
58
+ },
59
+ });
60
+ }
61
+ }
62
+
63
+ // 递归检查嵌套的语句块
64
+ if (statement.type === "IfStatement") {
65
+ // 检查 then 分支
66
+ if (statement.consequent.type === "BlockStatement") {
67
+ checkStatements(statement.consequent.body);
68
+ }
69
+ // 检查 else 分支
70
+ if (statement.alternate) {
71
+ if (statement.alternate.type === "BlockStatement") {
72
+ checkStatements(statement.alternate.body);
73
+ } else if (statement.alternate.type === "IfStatement") {
74
+ // else if 的情况
75
+ checkStatements([statement.alternate]);
76
+ }
77
+ }
78
+ } else if (statement.type === "SwitchStatement") {
79
+ // 检查 switch 的每个 case
80
+ statement.cases.forEach((caseClause) => {
81
+ checkStatements(caseClause.consequent);
82
+ });
83
+ } else if (
84
+ statement.type === "ForStatement" ||
85
+ statement.type === "WhileStatement" ||
86
+ statement.type === "DoWhileStatement"
87
+ ) {
88
+ // 检查循环体
89
+ if (statement.body.type === "BlockStatement") {
90
+ checkStatements(statement.body.body);
91
+ }
92
+ } else if (
93
+ statement.type === "ForInStatement" ||
94
+ statement.type === "ForOfStatement"
95
+ ) {
96
+ // 检查 for...in 和 for...of 循环体
97
+ if (statement.body.type === "BlockStatement") {
98
+ checkStatements(statement.body.body);
99
+ }
100
+ } else if (statement.type === "TryStatement") {
101
+ // 检查 try 块
102
+ if (statement.block.type === "BlockStatement") {
103
+ checkStatements(statement.block.body);
104
+ }
105
+ // 检查 catch 块
106
+ if (statement.handler && statement.handler.body.type === "BlockStatement") {
107
+ checkStatements(statement.handler.body.body);
108
+ }
109
+ // 检查 finally 块
110
+ if (statement.finalizer && statement.finalizer.type === "BlockStatement") {
111
+ checkStatements(statement.finalizer.body);
112
+ }
113
+ }
114
+ });
115
+ }
116
+
117
+ /**
118
+ * 检查方法体中是否有 return null
119
+ */
120
+ function checkReturnNull(
121
+ node: import("estree").MethodDefinition | import("estree").Property
122
+ ) {
123
+ // 只检查 render 方法
124
+ const isRenderMethod =
125
+ (node.key.type === "Identifier" && node.key.name === "render") ||
126
+ (node.key.type === "Literal" && node.key.value === "render");
127
+
128
+ if (!isRenderMethod) return;
129
+
130
+ // 获取方法体
131
+ const methodValue = node.type === "MethodDefinition" ? node.value : node.value;
132
+ if (!methodValue || methodValue.type !== "FunctionExpression") return;
133
+
134
+ const body = methodValue.body;
135
+ if (!body || body.type !== "BlockStatement") return;
136
+
137
+ // 递归检查所有语句(包括嵌套的)
138
+ checkStatements(body.body);
139
+ }
140
+
141
+ return {
142
+ // 检查类方法定义
143
+ MethodDefinition(node: import("estree").MethodDefinition) {
144
+ // 检查是否继承自 WebComponent 或 LightComponent
145
+ // 使用 getAncestors 获取父节点链(不包括当前节点)
146
+ const ancestors = context.getAncestors();
147
+
148
+ // 查找最近的 ClassDeclaration 父节点(从后往前)
149
+ let classDeclaration: import("estree").ClassDeclaration | null = null;
150
+ for (let i = ancestors.length - 1; i >= 0; i--) {
151
+ const ancestor = ancestors[i];
152
+ if (ancestor && ancestor.type === "ClassDeclaration") {
153
+ classDeclaration = ancestor as import("estree").ClassDeclaration;
154
+ break;
155
+ }
156
+ }
157
+
158
+ if (classDeclaration && classDeclaration.superClass) {
159
+ const isWSXComponent =
160
+ classDeclaration.superClass.type === "Identifier" &&
161
+ (classDeclaration.superClass.name === "WebComponent" ||
162
+ classDeclaration.superClass.name === "LightComponent");
163
+
164
+ if (isWSXComponent) {
165
+ checkReturnNull(node);
166
+ }
167
+ }
168
+ },
169
+ };
170
+ },
171
+ };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * ESLint 规则:require-jsx-import-source
3
+ *
4
+ * 要求 .wsx 文件包含 @jsxImportSource pragma 注释
5
+ * 自动修复:如果缺失,自动在文件开头添加
6
+ *
7
+ * 为什么需要这个规则:
8
+ * - IDE 的 TypeScript 语言服务器读取源文件,需要 pragma 来正确进行类型检查
9
+ * - Vite 插件在构建时注入 pragma 对 IDE 无效(IDE 看不到构建后的代码)
10
+ * - ESLint 自动修复直接修改源文件,IDE 可以立即看到 pragma
11
+ * - 这是唯一能让 IDE 和构建工具都正常工作的方案
12
+ */
13
+
14
+ import { Rule } from "eslint";
15
+ import { WSXRuleModule } from "../types";
16
+
17
+ const JSX_IMPORT_SOURCE_PRAGMA = "/** @jsxImportSource @wsxjs/wsx-core */";
18
+
19
+ export const requireJsxImportSource: WSXRuleModule = {
20
+ meta: {
21
+ type: "problem",
22
+ docs: {
23
+ description: "require @jsxImportSource pragma in .wsx files",
24
+ category: "Best Practices",
25
+ recommended: true,
26
+ },
27
+ fixable: "code",
28
+ messages: {
29
+ missingPragma:
30
+ "WSX files must include '@jsxImportSource @wsxjs/wsx-core' pragma comment at the top for IDE TypeScript language server support",
31
+ },
32
+ schema: [], // 无配置选项
33
+ },
34
+ defaultOptions: [], // 无默认配置选项
35
+ create(context: Rule.RuleContext) {
36
+ const filename = context.getFilename();
37
+ // 只检查 .wsx 文件
38
+ if (!filename.endsWith(".wsx")) {
39
+ return {};
40
+ }
41
+
42
+ const sourceCode = context.getSourceCode();
43
+ const text = sourceCode.getText();
44
+
45
+ // 检查是否已包含 pragma
46
+ const hasPragma = text.includes("@jsxImportSource");
47
+
48
+ if (!hasPragma) {
49
+ // 获取第一个节点(通常是文件开头的注释或 import)
50
+ const firstNode = sourceCode.ast.body[0] || sourceCode.ast;
51
+
52
+ context.report({
53
+ node: firstNode,
54
+ messageId: "missingPragma",
55
+ fix(fixer) {
56
+ // 在文件开头插入 pragma 注释
57
+ return fixer.insertTextBeforeRange([0, 0], `${JSX_IMPORT_SOURCE_PRAGMA}\n`);
58
+ },
59
+ });
60
+ }
61
+
62
+ return {};
63
+ },
64
+ };
package/src/types.ts CHANGED
@@ -12,6 +12,7 @@ export interface WSXRuleContext extends Rule.RuleContext {
12
12
  export interface WSXRuleModule extends Rule.RuleModule {
13
13
  // WSX 特定的规则模块扩展
14
14
  create: (context: WSXRuleContext) => Rule.RuleListener;
15
+ defaultOptions?: unknown[]; // 默认配置选项(可选)
15
16
  }
16
17
 
17
18
  export interface WSXConfig {