@lms5400/eslint-plugin-rsx 1.0.0
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 +0 -0
- package/README.md +191 -0
- package/index.cjs +7 -0
- package/package.json +33 -0
- package/rules/allow-unused-ctx-vars.cjs +44 -0
- package/rules/no-react-hooks.cjs +34 -0
- package/rules/no-use-ref.cjs +26 -0
- package/rules/require-ctx-destructure.cjs +39 -0
package/LICENSE
ADDED
|
File without changes
|
package/README.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# @lms5400/eslint-plugin-rsx
|
|
2
|
+
|
|
3
|
+
ESLint rules for **RSX components**.
|
|
4
|
+
|
|
5
|
+
RSX files use JSX syntax but **do not follow React’s runtime or hook model**.
|
|
6
|
+
This plugin enforces RSX-specific semantics while remaining compatible with React projects.
|
|
7
|
+
|
|
8
|
+
See https://github.com/LMS007/babel-plugin-rsx
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## What this plugin enforces
|
|
13
|
+
|
|
14
|
+
RSX components are **not React components**. This plugin helps enforce that distinction by:
|
|
15
|
+
|
|
16
|
+
- Disallowing React hooks (`useState`, `useEffect`, etc.)
|
|
17
|
+
- Disallowing `useRef`
|
|
18
|
+
- Requiring RSX context destructuring
|
|
19
|
+
- Allowing JSX syntax
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Requirements
|
|
24
|
+
|
|
25
|
+
- **ESLint ≥ 8.57** (flat config)
|
|
26
|
+
- Node.js ≥ 18
|
|
27
|
+
- [babel-plugin-rsx](https://www.npmjs.com/package/@lms5400/babel-plugin-rsx/)
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
npm
|
|
33
|
+
```bash
|
|
34
|
+
npm install --save-dev eslint @lms5400/eslint-plugin-rsx
|
|
35
|
+
```
|
|
36
|
+
Or yarn
|
|
37
|
+
```bash
|
|
38
|
+
yarn add -D eslint @lms5400/eslint-plugin-rsx
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Basic setup (React + RSX)
|
|
44
|
+
|
|
45
|
+
This setup assumes:
|
|
46
|
+
- `.jsx / .tsx` files use normal React rules
|
|
47
|
+
- `.rsx` files use RSX rules
|
|
48
|
+
- ESLint flat config is enabled
|
|
49
|
+
|
|
50
|
+
### eslint.config.js (or eslint.config.cjs)
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
import js from "@eslint/js";
|
|
54
|
+
import globals from "globals";
|
|
55
|
+
import react from "eslint-plugin-react";
|
|
56
|
+
import reactHooks from "eslint-plugin-react-hooks";
|
|
57
|
+
import rsx from "@lms5400/eslint-plugin-rsx";
|
|
58
|
+
|
|
59
|
+
export default [
|
|
60
|
+
js.configs.recommended,
|
|
61
|
+
|
|
62
|
+
// ----------------------------------------
|
|
63
|
+
// React JSX / TSX files
|
|
64
|
+
// ----------------------------------------
|
|
65
|
+
{
|
|
66
|
+
files: ["**/*.{jsx,tsx}"],
|
|
67
|
+
plugins: {
|
|
68
|
+
react,
|
|
69
|
+
"react-hooks": reactHooks,
|
|
70
|
+
},
|
|
71
|
+
rules: {
|
|
72
|
+
// React + hooks rules
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
// ----------------------------------------
|
|
77
|
+
// RSX files
|
|
78
|
+
// ----------------------------------------
|
|
79
|
+
{
|
|
80
|
+
files: ["**/*.rsx"],
|
|
81
|
+
plugins: {
|
|
82
|
+
rsx,
|
|
83
|
+
},
|
|
84
|
+
languageOptions: {
|
|
85
|
+
ecmaVersion: "latest",
|
|
86
|
+
sourceType: "module",
|
|
87
|
+
globals: {
|
|
88
|
+
...globals.browser,
|
|
89
|
+
...globals.node,
|
|
90
|
+
},
|
|
91
|
+
parserOptions: {
|
|
92
|
+
ecmaFeatures: { jsx: true },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
rules: {
|
|
96
|
+
"rsx/no-react-hooks": "error",
|
|
97
|
+
"rsx/no-use-ref": "warn",
|
|
98
|
+
"rsx/require-ctx-destructure": "error",
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Why React rules are not applied to `.rsx`
|
|
107
|
+
|
|
108
|
+
RSX files:
|
|
109
|
+
- may contain JSX
|
|
110
|
+
- do **not** use React hooks
|
|
111
|
+
- do **not** rely on React reconciliation
|
|
112
|
+
- have explicit lifecycle management
|
|
113
|
+
|
|
114
|
+
Applying `eslint-plugin-react` rules to `.rsx` files often produces **incorrect or misleading errors**.
|
|
115
|
+
|
|
116
|
+
If you want minimal JSX hygiene, you may selectively enable rules like:
|
|
117
|
+
- `react/jsx-no-undef`
|
|
118
|
+
- `react/jsx-uses-vars`
|
|
119
|
+
|
|
120
|
+
Full React recommended rules are **not advised** for RSX.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## VS Code setup (required)
|
|
125
|
+
|
|
126
|
+
The ESLint VS Code extension does **not** automatically lint custom extensions like `.rsx`.
|
|
127
|
+
|
|
128
|
+
Add the following to your workspace settings.
|
|
129
|
+
|
|
130
|
+
### .vscode/settings.json
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"eslint.useFlatConfig": true,
|
|
135
|
+
"eslint.validate": [
|
|
136
|
+
"javascript",
|
|
137
|
+
"javascriptreact",
|
|
138
|
+
"rsx"
|
|
139
|
+
],
|
|
140
|
+
"files.associations": {
|
|
141
|
+
"*.rsx": "javascriptreact"
|
|
142
|
+
},
|
|
143
|
+
"eslint.nodePath": "./node_modules"
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
After changing settings:
|
|
148
|
+
1. Open Command Palette
|
|
149
|
+
2. Run **“ESLint: Restart ESLint Server”**
|
|
150
|
+
3. Reload the window if needed
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Verifying the setup
|
|
155
|
+
|
|
156
|
+
Create a test RSX file:
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
export default function Test(ctx) {
|
|
160
|
+
useState(0);
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Expected:
|
|
165
|
+
- ❌ ESLint error
|
|
166
|
+
- Rule: `rsx/no-react-hooks`
|
|
167
|
+
- Visible inline and in the Problems panel
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Rules
|
|
172
|
+
|
|
173
|
+
| Rule | Description |
|
|
174
|
+
|------|-------------|
|
|
175
|
+
| `rsx/no-react-hooks` | Disallow React hooks in RSX |
|
|
176
|
+
| `rsx/no-use-ref` | Disallow `useRef` |
|
|
177
|
+
| `rsx/require-ctx-destructure` | Require RSX context destructuring |
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Design philosophy
|
|
182
|
+
|
|
183
|
+
- RSX is explicit and lifecycle-driven
|
|
184
|
+
- JSX is syntax, not a runtime commitment
|
|
185
|
+
- Linting should enforce RSX semantics, not React assumptions
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
MIT
|
package/index.cjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lms5400/eslint-plugin-rsx",
|
|
3
|
+
"private": false,
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "https://github.com/LMS007/eslint-plugin-rsx.git"
|
|
7
|
+
},
|
|
8
|
+
"version": "1.0.0",
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18"
|
|
11
|
+
},
|
|
12
|
+
"description": "ESLint rules for RSX components",
|
|
13
|
+
"main": "index.cjs",
|
|
14
|
+
"type": "commonjs",
|
|
15
|
+
"files": [
|
|
16
|
+
"index.cjs",
|
|
17
|
+
"rules"
|
|
18
|
+
],
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"eslint": "^8.0.0 || ^9.0.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"eslint": "^9.0.0",
|
|
24
|
+
"eslint-rule-tester": "^2.0.0"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"keywords": [
|
|
28
|
+
"eslint",
|
|
29
|
+
"eslint-plugin",
|
|
30
|
+
"rsx",
|
|
31
|
+
"react",
|
|
32
|
+
"lint"]
|
|
33
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const ALLOWED_ARGS = /^(ctx|view|render|update|destroy|props)$/;
|
|
4
|
+
const ALLOWED_VARS = /^(view|render|update|destroy|props)$/;
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
meta: {
|
|
8
|
+
type: "problem",
|
|
9
|
+
docs: {
|
|
10
|
+
description: "Ignore unused RSX context lifecycle variables",
|
|
11
|
+
},
|
|
12
|
+
schema: [],
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
create(context) {
|
|
16
|
+
function isIgnored(variable) {
|
|
17
|
+
const name = variable.name;
|
|
18
|
+
|
|
19
|
+
// Function parameters (destructured ctx)
|
|
20
|
+
if (variable.defs.some(d => d.type === "Parameter")) {
|
|
21
|
+
return ALLOWED_ARGS.test(name);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Local variables
|
|
25
|
+
return ALLOWED_VARS.test(name);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
"Program:exit"() {
|
|
30
|
+
const scope = context.getScope();
|
|
31
|
+
|
|
32
|
+
for (const variable of scope.variables) {
|
|
33
|
+
if (isIgnored(variable)) continue;
|
|
34
|
+
if (variable.references.length === 0) {
|
|
35
|
+
context.report({
|
|
36
|
+
node: variable.identifiers[0],
|
|
37
|
+
message: `'${variable.name}' is defined but never used.`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const BANNED = new Set([
|
|
2
|
+
"useState",
|
|
3
|
+
"useEffect",
|
|
4
|
+
"useLayoutEffect",
|
|
5
|
+
"useReducer",
|
|
6
|
+
"useCallback",
|
|
7
|
+
"useMemo",
|
|
8
|
+
"useContext"
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
meta: {
|
|
13
|
+
type: "problem",
|
|
14
|
+
docs: {
|
|
15
|
+
description: "Disallow React hooks in RSX files"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
create(context) {
|
|
20
|
+
return {
|
|
21
|
+
CallExpression(node) {
|
|
22
|
+
if (
|
|
23
|
+
node.callee.type === "Identifier" &&
|
|
24
|
+
BANNED.has(node.callee.name)
|
|
25
|
+
) {
|
|
26
|
+
context.report({
|
|
27
|
+
node,
|
|
28
|
+
message: `${node.callee.name} is not allowed in RSX components`
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: "problem",
|
|
4
|
+
docs: {
|
|
5
|
+
description: "Disallow useRef in RSX components"
|
|
6
|
+
}
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
create(context) {
|
|
10
|
+
return {
|
|
11
|
+
CallExpression(node) {
|
|
12
|
+
if (
|
|
13
|
+
node.callee.type === "Identifier" &&
|
|
14
|
+
node.callee.name === "useRef"
|
|
15
|
+
) {
|
|
16
|
+
context.report({
|
|
17
|
+
node,
|
|
18
|
+
message:
|
|
19
|
+
"useRef is not allowed in RSX components. " +
|
|
20
|
+
"Use regular variables instead."
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: "problem",
|
|
4
|
+
docs: {
|
|
5
|
+
description: "Require RSX context parameter to be destructured"
|
|
6
|
+
}
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
create(context) {
|
|
10
|
+
function check(node) {
|
|
11
|
+
if (!node.params || node.params.length !== 1) return;
|
|
12
|
+
|
|
13
|
+
const param = node.params[0];
|
|
14
|
+
|
|
15
|
+
if (param.type === "Identifier") {
|
|
16
|
+
context.report({
|
|
17
|
+
node: param,
|
|
18
|
+
message:
|
|
19
|
+
"RSX components must destructure the context parameter. " +
|
|
20
|
+
"Use: function Component({ view, update, render, destroy, props }) { ... }"
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
ExportDefaultDeclaration(node) {
|
|
27
|
+
const decl = node.declaration;
|
|
28
|
+
if (
|
|
29
|
+
decl &&
|
|
30
|
+
(decl.type === "FunctionDeclaration" ||
|
|
31
|
+
decl.type === "FunctionExpression" ||
|
|
32
|
+
decl.type === "ArrowFunctionExpression")
|
|
33
|
+
) {
|
|
34
|
+
check(decl);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
};
|