@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 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
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ rules: {
3
+ "no-react-hooks": require("./rules/no-react-hooks.cjs"),
4
+ "no-use-ref": require("./rules/no-use-ref.cjs"),
5
+ "require-ctx-destructure": require("./rules/require-ctx-destructure.cjs"),
6
+ },
7
+ };
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
+ };