@tsrx/eslint-plugin 0.3.25

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
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Dominic Gannaway
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,260 @@
1
+ # @tsrx/eslint-plugin
2
+
3
+ [![npm version](https://img.shields.io/npm/v/%40tsrx%2Feslint-plugin?logo=npm)](https://www.npmjs.com/package/@tsrx/eslint-plugin)
4
+ [![npm downloads](https://img.shields.io/npm/dm/%40tsrx%2Feslint-plugin?logo=npm&label=downloads)](https://www.npmjs.com/package/@tsrx/eslint-plugin)
5
+
6
+ ESLint plugin for [Ripple](https://ripplejs.com) - helps enforce best practices
7
+ and catch common mistakes when writing Ripple applications.
8
+
9
+ Works just like `eslint-plugin-react` - simply install and use the recommended
10
+ config!
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install --save-dev '@tsrx/eslint-plugin'
16
+ # or
17
+ yarn add --dev '@tsrx/eslint-plugin'
18
+ # or
19
+ pnpm add --save-dev '@tsrx/eslint-plugin'
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ### Flat Config (ESLint 9+)
25
+
26
+ ```js
27
+ // eslint.config.js
28
+ import ripple from '@tsrx/eslint-plugin';
29
+
30
+ export default [...ripple.configs.recommended];
31
+ ```
32
+
33
+ The plugin automatically:
34
+
35
+ - Detects and uses `@tsrx/eslint-parser` if installed for `.tsrx` files
36
+ - Detects and uses `@typescript-eslint/parser` if installed for `.ts`/`.tsx` files
37
+ - Excludes `.d.ts` files, `node_modules`, `dist`, and `build` directories from
38
+ linting
39
+ - Works with both `.ts`/`.tsx` and Ripple component files (`.tsrx` by default,
40
+ plus `.tsrx`)
41
+
42
+ ### Legacy Config (.eslintrc)
43
+
44
+ ```json
45
+ {
46
+ "plugins": ["ripple"],
47
+ "extends": ["plugin:ripple/recommended"]
48
+ }
49
+ ```
50
+
51
+ ## Configurations
52
+
53
+ ### `recommended`
54
+
55
+ The recommended configuration enables all rules at their default severity levels
56
+ (errors and warnings).
57
+
58
+ ```js
59
+ import ripple from '@tsrx/eslint-plugin';
60
+
61
+ export default [
62
+ {
63
+ plugins: { ripple },
64
+ rules: ripple.configs.recommended.rules,
65
+ },
66
+ ];
67
+ ```
68
+
69
+ ### `strict`
70
+
71
+ The strict configuration enables all rules as errors.
72
+
73
+ ```js
74
+ import ripple from '@tsrx/eslint-plugin';
75
+
76
+ export default [
77
+ {
78
+ plugins: { ripple },
79
+ rules: ripple.configs.strict.rules,
80
+ },
81
+ ];
82
+ ```
83
+
84
+ ## Rules
85
+
86
+ ### `ripple/no-module-scope-track` (error)
87
+
88
+ Prevents calling `track()` at module scope. Tracked values must be created within
89
+ a component context.
90
+
91
+ ❌ **Incorrect:**
92
+
93
+ ```js
94
+ import { track } from 'ripple';
95
+
96
+ // This will cause runtime errors
97
+ let globalCount = track(0);
98
+
99
+ export component App() {
100
+ <div>{@globalCount}</div>
101
+ }
102
+ ```
103
+
104
+ ✅ **Correct:**
105
+
106
+ ```js
107
+ import { track } from 'ripple';
108
+
109
+ export component App() {
110
+ // track() called within component
111
+ let count = track(0);
112
+
113
+ <div>{@count}</div>
114
+ }
115
+ ```
116
+
117
+ ### `ripple/require-component-export` (warning)
118
+
119
+ Warns when capitalized components are not exported. This helps ensure components
120
+ are reusable across modules.
121
+
122
+ ❌ **Incorrect:**
123
+
124
+ ```js
125
+ component MyButton() {
126
+ <button>Click me</button>
127
+ }
128
+ // MyButton is defined but not exported
129
+ ```
130
+
131
+ ✅ **Correct:**
132
+
133
+ ```js
134
+ export component MyButton() {
135
+ <button>Click me</button>
136
+ }
137
+ ```
138
+
139
+ ### `ripple/prefer-oninput` (warning, fixable)
140
+
141
+ Recommends using `onInput` instead of `onChange` for form inputs. Unlike React,
142
+ Ripple doesn't have synthetic events, so `onInput` is the correct event handler.
143
+
144
+ ❌ **Incorrect:**
145
+
146
+ ```jsx
147
+ <input onChange={handleChange} />
148
+ ```
149
+
150
+ ✅ **Correct:**
151
+
152
+ ```jsx
153
+ <input onInput={handleInput} />
154
+ ```
155
+
156
+ This rule is auto-fixable with `--fix`.
157
+
158
+ ### `ripple/no-return-in-component` (error)
159
+
160
+ Prevents returning JSX from Ripple components. In Ripple, JSX should be used as
161
+ statements, not expressions.
162
+
163
+ ❌ **Incorrect:**
164
+
165
+ ```js
166
+ export component App() {
167
+ return <div>Hello World</div>;
168
+ }
169
+ ```
170
+
171
+ ✅ **Correct:**
172
+
173
+ ```js
174
+ export component App() {
175
+ <div>Hello World</div>
176
+ }
177
+ ```
178
+
179
+ ### `ripple/no-lazy-destructuring-in-modules` (error)
180
+
181
+ Prevents using lazy destructuring (`&[]` / `&{}`) in TypeScript/JavaScript
182
+ modules. In `.ts`/`.js` files, you should use `.value` to read and write tracked
183
+ values instead.
184
+
185
+ ❌ **Incorrect:**
186
+
187
+ ```ts
188
+ // count.ts
189
+ import { track, effect } from 'ripple';
190
+
191
+ export function useCount() {
192
+ const &[count] = track(1);
193
+ effect(() => {
194
+ console.log(count); // Error: Cannot use &[] in TypeScript modules
195
+ });
196
+ return { count };
197
+ }
198
+ ```
199
+
200
+ ✅ **Correct:**
201
+
202
+ ```ts
203
+ // count.ts
204
+ import { track, effect } from 'ripple';
205
+
206
+ export function useCount() {
207
+ const count = track(1);
208
+
209
+ // Use .value to read tracked values
210
+ const double = track(() => count.value * 2);
211
+
212
+ effect(() => {
213
+ console.log('count is', count.value);
214
+ });
215
+
216
+ return { count, double };
217
+ }
218
+ ```
219
+
220
+ **Note:** Lazy destructuring (`&[]` / `&{}`) is only valid in Ripple component
221
+ files (`.tsrx` by default). In TypeScript modules, use `.value` to read and write
222
+ tracked values.
223
+
224
+ ## Custom Configuration
225
+
226
+ You can customize individual rules in your ESLint config:
227
+
228
+ ```js
229
+ export default [
230
+ {
231
+ plugins: { ripple },
232
+ rules: {
233
+ 'ripple/no-module-scope-track': 'error',
234
+ 'ripple/require-component-export': 'off', // Disable this rule
235
+ 'ripple/prefer-oninput': 'error', // Make this an error instead of warning
236
+ 'ripple/no-return-in-component': 'error',
237
+ 'ripple/no-lazy-destructuring-in-modules': 'error',
238
+ },
239
+ },
240
+ ];
241
+ ```
242
+
243
+ The plugin will automatically detect and use the Ripple parser for your Ripple
244
+ component files, including `.tsrx`.
245
+
246
+ ## License
247
+
248
+ MIT
249
+
250
+ ## Contributing
251
+
252
+ Contributions are welcome! Please feel free to submit a Pull Request.
253
+
254
+ ## Related
255
+
256
+ - [Ripple](https://ripplejs.com) - The Ripple framework
257
+ - [@ripple-ts/vite-plugin](https://www.npmjs.com/package/@ripple-ts/vite-plugin) -
258
+ Vite plugin for Ripple
259
+ - [@ripple-ts/prettier-plugin](https://www.npmjs.com/package/@ripple-ts/prettier-plugin) -
260
+ Prettier plugin for Ripple
@@ -0,0 +1,21 @@
1
+ import * as eslint from "eslint";
2
+
3
+ //#region src/index.d.ts
4
+ declare const plugin: {
5
+ meta: {
6
+ name: string;
7
+ version: string;
8
+ };
9
+ rules: {
10
+ 'no-module-scope-track': eslint.Rule.RuleModule;
11
+ 'prefer-oninput': eslint.Rule.RuleModule;
12
+ 'no-return-in-component': eslint.Rule.RuleModule;
13
+ 'control-flow-jsx': eslint.Rule.RuleModule;
14
+ 'no-lazy-destructuring-in-modules': eslint.Rule.RuleModule;
15
+ 'valid-for-of-key': eslint.Rule.RuleModule;
16
+ };
17
+ configs: any;
18
+ };
19
+ //#endregion
20
+ export { plugin as default };
21
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;cAQM,MAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,386 @@
1
+ import { createRequire } from "module";
2
+
3
+ //#region src/rules/no-module-scope-track.ts
4
+ const rule$5 = {
5
+ meta: {
6
+ type: "problem",
7
+ docs: {
8
+ description: "Disallow calling track() at module scope",
9
+ recommended: true
10
+ },
11
+ messages: { moduleScope: "track() cannot be called at module scope. It must be called within a component context." },
12
+ schema: []
13
+ },
14
+ create(context) {
15
+ let componentDepth = 0;
16
+ let functionDepth = 0;
17
+ const incrementComponentDepth = () => componentDepth++;
18
+ const decrementComponentDepth = () => componentDepth--;
19
+ const incrementFunctionDepth = () => functionDepth++;
20
+ const decrementFunctionDepth = () => functionDepth--;
21
+ return {
22
+ Component: incrementComponentDepth,
23
+ "Component:exit": decrementComponentDepth,
24
+ FunctionDeclaration: incrementFunctionDepth,
25
+ "FunctionDeclaration:exit": decrementFunctionDepth,
26
+ FunctionExpression: incrementFunctionDepth,
27
+ "FunctionExpression:exit": decrementFunctionDepth,
28
+ ArrowFunctionExpression: incrementFunctionDepth,
29
+ "ArrowFunctionExpression:exit": decrementFunctionDepth,
30
+ CallExpression(node) {
31
+ if (node.callee.type === "Identifier" && node.callee.name === "track" && componentDepth === 0 && functionDepth === 0) context.report({
32
+ node,
33
+ messageId: "moduleScope"
34
+ });
35
+ }
36
+ };
37
+ }
38
+ };
39
+
40
+ //#endregion
41
+ //#region src/rules/prefer-oninput.ts
42
+ const rule$4 = {
43
+ meta: {
44
+ type: "suggestion",
45
+ docs: {
46
+ description: "Prefer onInput over onChange for form inputs in Ripple",
47
+ recommended: true
48
+ },
49
+ messages: { preferOnInput: "Use \"onInput\" instead of \"onChange\". Ripple does not have synthetic events like React." },
50
+ fixable: "code",
51
+ schema: []
52
+ },
53
+ create(context) {
54
+ const reported_ranges = /* @__PURE__ */ new Set();
55
+ function report_onchange(node) {
56
+ const range = node.range;
57
+ if (!range) return;
58
+ const key = `${range[0]}:${range[1]}`;
59
+ if (reported_ranges.has(key)) return;
60
+ reported_ranges.add(key);
61
+ context.report({
62
+ node,
63
+ messageId: "preferOnInput",
64
+ fix(fixer) {
65
+ return fixer.replaceText(node.name, "onInput");
66
+ }
67
+ });
68
+ }
69
+ return {
70
+ "JSXAttribute[name.name=\"onChange\"]"(node) {
71
+ report_onchange(node);
72
+ },
73
+ "Attribute[name.name=\"onChange\"]"(node) {
74
+ report_onchange(node);
75
+ },
76
+ "Property[key.name=\"onChange\"]"(node) {
77
+ if (context.sourceCode.getAncestors(node).some((ancestor) => ancestor.type === "ObjectExpression")) context.report({
78
+ node,
79
+ messageId: "preferOnInput",
80
+ fix(fixer) {
81
+ return fixer.replaceText(node.key, "onInput");
82
+ }
83
+ });
84
+ }
85
+ };
86
+ }
87
+ };
88
+
89
+ //#endregion
90
+ //#region src/rules/no-return-in-component.ts
91
+ const rule$3 = {
92
+ meta: {
93
+ type: "problem",
94
+ docs: {
95
+ description: "Disallow return statements with JSX in Ripple components",
96
+ recommended: true
97
+ },
98
+ messages: { noReturn: "Do not return JSX from Ripple components. Use JSX as statements instead." },
99
+ schema: []
100
+ },
101
+ create(context) {
102
+ let insideComponent = 0;
103
+ return {
104
+ "ExpressionStatement > CallExpression[callee.name='component']"() {
105
+ insideComponent++;
106
+ },
107
+ "ExpressionStatement > CallExpression[callee.name='component']:exit"() {
108
+ insideComponent--;
109
+ },
110
+ "VariableDeclarator[init.callee.name=\"component\"]"() {
111
+ insideComponent++;
112
+ },
113
+ "VariableDeclarator[init.callee.name=\"component\"]:exit"() {
114
+ insideComponent--;
115
+ },
116
+ ReturnStatement(node) {
117
+ if (insideComponent > 0 && node.argument) {
118
+ if (node.argument.type === "JSXElement" || node.argument.type === "JSXFragment") context.report({
119
+ node,
120
+ messageId: "noReturn"
121
+ });
122
+ }
123
+ }
124
+ };
125
+ }
126
+ };
127
+
128
+ //#endregion
129
+ //#region src/rules/control-flow-jsx.ts
130
+ const rule$2 = {
131
+ meta: {
132
+ type: "problem",
133
+ docs: {
134
+ description: "Require JSX in for...of loops within components, but disallow JSX in for...of loops within effects",
135
+ recommended: true
136
+ },
137
+ messages: {
138
+ requireJsxInLoop: "For...of loops in component bodies should contain JSX elements. Use JSX to render items.",
139
+ noJsxInEffectLoop: "For...of loops inside effect() should not contain JSX. Effects are for side effects, not rendering."
140
+ },
141
+ schema: []
142
+ },
143
+ create(context) {
144
+ let insideComponent = 0;
145
+ let insideEffect = 0;
146
+ function containsJSX(node, visited = /* @__PURE__ */ new Set()) {
147
+ if (!node) return false;
148
+ if (visited.has(node)) return false;
149
+ visited.add(node);
150
+ if (node.type === "JSXElement" || node.type === "JSXFragment" || node.type === "Element") return true;
151
+ const keys = Object.keys(node);
152
+ for (const key of keys) {
153
+ if (key === "parent" || key === "loc" || key === "range") continue;
154
+ const value = node[key];
155
+ if (value && typeof value === "object") {
156
+ if (Array.isArray(value)) {
157
+ for (const item of value) if (item && typeof item === "object" && containsJSX(item, visited)) return true;
158
+ } else if (value.type && containsJSX(value, visited)) return true;
159
+ }
160
+ }
161
+ return false;
162
+ }
163
+ return {
164
+ Component() {
165
+ insideComponent++;
166
+ },
167
+ "Component:exit"() {
168
+ insideComponent--;
169
+ },
170
+ "CallExpression[callee.name='effect']"() {
171
+ insideEffect++;
172
+ },
173
+ "CallExpression[callee.name='effect']:exit"() {
174
+ insideEffect--;
175
+ },
176
+ ForOfStatement(node) {
177
+ if (insideComponent === 0) return;
178
+ const hasJSX = containsJSX(node.body);
179
+ if (insideEffect > 0) {
180
+ if (hasJSX) context.report({
181
+ node,
182
+ messageId: "noJsxInEffectLoop"
183
+ });
184
+ } else if (!hasJSX) context.report({
185
+ node,
186
+ messageId: "requireJsxInLoop"
187
+ });
188
+ }
189
+ };
190
+ }
191
+ };
192
+
193
+ //#endregion
194
+ //#region src/rules/no-lazy-destructuring-in-modules.ts
195
+ const rule$1 = {
196
+ meta: {
197
+ type: "problem",
198
+ docs: {
199
+ description: "Disallow lazy destructuring (&[] / &{}) in TypeScript/JavaScript modules",
200
+ recommended: true
201
+ },
202
+ messages: { noLazyDestructuring: "Lazy destructuring (&[] / &{}) cannot be used in TypeScript/JavaScript modules. Use .value to read and write tracked values instead." },
203
+ schema: []
204
+ },
205
+ create(context) {
206
+ const filename = context.filename;
207
+ if (filename && filename.endsWith(".tsrx")) return {};
208
+ return {
209
+ ArrayPattern(node) {
210
+ if (node.lazy === true) context.report({
211
+ node,
212
+ messageId: "noLazyDestructuring"
213
+ });
214
+ },
215
+ ObjectPattern(node) {
216
+ if (node.lazy === true) context.report({
217
+ node,
218
+ messageId: "noLazyDestructuring"
219
+ });
220
+ }
221
+ };
222
+ }
223
+ };
224
+
225
+ //#endregion
226
+ //#region src/rules/valid-for-of-key.ts
227
+ const rule = {
228
+ meta: {
229
+ type: "problem",
230
+ docs: {
231
+ description: "Ensure variables used in for..of key expression are defined",
232
+ recommended: true
233
+ },
234
+ messages: { undefinedVariable: "Variable '{{name}}' is not defined." },
235
+ schema: []
236
+ },
237
+ create(context) {
238
+ return { ForOfStatement(node) {
239
+ if (!node.key) return;
240
+ const checkIdentifier = (identifier) => {
241
+ if (!findVariable(context.sourceCode.getScope(node), identifier.name)) context.report({
242
+ node: identifier,
243
+ messageId: "undefinedVariable",
244
+ data: { name: identifier.name }
245
+ });
246
+ };
247
+ const traverse = (node) => {
248
+ if (!node) return;
249
+ switch (node.type) {
250
+ case "Identifier":
251
+ checkIdentifier(node);
252
+ break;
253
+ case "MemberExpression":
254
+ traverse(node.object);
255
+ if (node.computed) traverse(node.property);
256
+ break;
257
+ case "BinaryExpression":
258
+ case "LogicalExpression":
259
+ traverse(node.left);
260
+ traverse(node.right);
261
+ break;
262
+ case "UnaryExpression":
263
+ traverse(node.argument);
264
+ break;
265
+ case "CallExpression":
266
+ traverse(node.callee);
267
+ node.arguments.forEach(traverse);
268
+ break;
269
+ case "ArrayExpression":
270
+ node.elements.forEach(traverse);
271
+ break;
272
+ case "ObjectExpression":
273
+ node.properties.forEach((prop) => {
274
+ if (prop.type === "Property") {
275
+ if (prop.computed) traverse(prop.key);
276
+ traverse(prop.value);
277
+ } else if (prop.type === "SpreadElement") traverse(prop.argument);
278
+ });
279
+ break;
280
+ case "ConditionalExpression":
281
+ traverse(node.test);
282
+ traverse(node.consequent);
283
+ traverse(node.alternate);
284
+ break;
285
+ case "TemplateLiteral":
286
+ node.expressions.forEach(traverse);
287
+ break;
288
+ }
289
+ };
290
+ traverse(node.key);
291
+ } };
292
+ }
293
+ };
294
+ function findVariable(scope, name) {
295
+ let currentScope = scope;
296
+ while (currentScope) {
297
+ const variable = currentScope.variables.find((v) => v.name === name);
298
+ if (variable) return variable;
299
+ currentScope = currentScope.upper;
300
+ }
301
+ return null;
302
+ }
303
+
304
+ //#endregion
305
+ //#region src/index.ts
306
+ const plugin = {
307
+ meta: {
308
+ name: "@tsrx/eslint-plugin",
309
+ version: "0.1.3"
310
+ },
311
+ rules: {
312
+ "no-module-scope-track": rule$5,
313
+ "prefer-oninput": rule$4,
314
+ "no-return-in-component": rule$3,
315
+ "control-flow-jsx": rule$2,
316
+ "no-lazy-destructuring-in-modules": rule$1,
317
+ "valid-for-of-key": rule
318
+ },
319
+ configs: {}
320
+ };
321
+ const require = createRequire(import.meta.url);
322
+ let rippleParser;
323
+ let tsParser;
324
+ try {
325
+ rippleParser = require("@tsrx/eslint-parser");
326
+ } catch {
327
+ rippleParser = null;
328
+ }
329
+ try {
330
+ tsParser = require("@typescript-eslint/parser");
331
+ } catch {
332
+ tsParser = null;
333
+ }
334
+ function createConfig(name, files, parser) {
335
+ const config = {
336
+ name,
337
+ files,
338
+ plugins: { ripple: plugin },
339
+ rules: {
340
+ "ripple/no-module-scope-track": "error",
341
+ "ripple/prefer-oninput": "warn",
342
+ "ripple/no-return-in-component": "error",
343
+ "ripple/control-flow-jsx": "error",
344
+ "ripple/no-lazy-destructuring-in-modules": "error",
345
+ "ripple/valid-for-of-key": "error"
346
+ }
347
+ };
348
+ if (parser) config.languageOptions = {
349
+ parser,
350
+ parserOptions: {
351
+ ecmaVersion: "latest",
352
+ sourceType: "module"
353
+ }
354
+ };
355
+ return config;
356
+ }
357
+ plugin.configs.recommended = [
358
+ createConfig("ripple/recommended-ripple-files", ["**/*.tsrx"], rippleParser),
359
+ createConfig("ripple/recommended-typescript-files", ["**/*.ts", "**/*.tsx"], tsParser),
360
+ {
361
+ name: "ripple/ignores",
362
+ ignores: [
363
+ "**/*.d.ts",
364
+ "**/node_modules/**",
365
+ "**/dist/**",
366
+ "**/build/**"
367
+ ]
368
+ }
369
+ ];
370
+ plugin.configs.strict = [
371
+ createConfig("ripple/strict-ripple-files", ["**/*.tsrx"], rippleParser),
372
+ createConfig("ripple/strict-typescript-files", ["**/*.ts", "**/*.tsx"], tsParser),
373
+ {
374
+ name: "ripple/ignores",
375
+ ignores: [
376
+ "**/*.d.ts",
377
+ "**/node_modules/**",
378
+ "**/dist/**",
379
+ "**/build/**"
380
+ ]
381
+ }
382
+ ];
383
+
384
+ //#endregion
385
+ export { plugin as default };
386
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["rule","rule","rule","rule","rule","noModuleScopeTrack","preferOnInput","noReturnInComponent","controlFlowJsx","noLazyDestructuringInModules","validForOfKey"],"sources":["../src/rules/no-module-scope-track.ts","../src/rules/prefer-oninput.ts","../src/rules/no-return-in-component.ts","../src/rules/control-flow-jsx.ts","../src/rules/no-lazy-destructuring-in-modules.ts","../src/rules/valid-for-of-key.ts","../src/index.ts"],"sourcesContent":["import type { Rule } from 'eslint';\nimport type * as AST from '@tsrx/core/types/estree';\n\nconst rule: Rule.RuleModule = {\n\tmeta: {\n\t\ttype: 'problem',\n\t\tdocs: {\n\t\t\tdescription: 'Disallow calling track() at module scope',\n\t\t\trecommended: true,\n\t\t},\n\t\tmessages: {\n\t\t\tmoduleScope:\n\t\t\t\t'track() cannot be called at module scope. It must be called within a component context.',\n\t\t},\n\t\tschema: [],\n\t},\n\tcreate(context) {\n\t\tlet componentDepth = 0;\n\t\tlet functionDepth = 0;\n\n\t\tconst incrementComponentDepth = () => componentDepth++;\n\t\tconst decrementComponentDepth = () => componentDepth--;\n\t\tconst incrementFunctionDepth = () => functionDepth++;\n\t\tconst decrementFunctionDepth = () => functionDepth--;\n\n\t\treturn {\n\t\t\t// Only track when we enter a Ripple component\n\t\t\t// Ripple's parser returns \"Component\" nodes for component declarations\n\t\t\tComponent: incrementComponentDepth,\n\t\t\t'Component:exit': decrementComponentDepth,\n\n\t\t\t// Track regular functions and arrow functions\n\t\t\tFunctionDeclaration: incrementFunctionDepth,\n\t\t\t'FunctionDeclaration:exit': decrementFunctionDepth,\n\t\t\tFunctionExpression: incrementFunctionDepth,\n\t\t\t'FunctionExpression:exit': decrementFunctionDepth,\n\t\t\tArrowFunctionExpression: incrementFunctionDepth,\n\t\t\t'ArrowFunctionExpression:exit': decrementFunctionDepth,\n\n\t\t\t// Check track() calls\n\t\t\tCallExpression(node: AST.CallExpression) {\n\t\t\t\tif (\n\t\t\t\t\tnode.callee.type === 'Identifier' &&\n\t\t\t\t\tnode.callee.name === 'track' &&\n\t\t\t\t\tcomponentDepth === 0 &&\n\t\t\t\t\tfunctionDepth === 0\n\t\t\t\t) {\n\t\t\t\t\tcontext.report({\n\t\t\t\t\t\tnode,\n\t\t\t\t\t\tmessageId: 'moduleScope',\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t},\n};\n\nexport default rule;\n","import type { Rule } from 'eslint';\nimport type * as AST from '@tsrx/core/types/estree';\nimport type * as ESTreeJSX from '@tsrx/core/types/estree-jsx';\n\nconst rule: Rule.RuleModule = {\n\tmeta: {\n\t\ttype: 'suggestion',\n\t\tdocs: {\n\t\t\tdescription: 'Prefer onInput over onChange for form inputs in Ripple',\n\t\t\trecommended: true,\n\t\t},\n\t\tmessages: {\n\t\t\tpreferOnInput:\n\t\t\t\t'Use \"onInput\" instead of \"onChange\". Ripple does not have synthetic events like React.',\n\t\t},\n\t\tfixable: 'code',\n\t\tschema: [],\n\t},\n\tcreate(context) {\n\t\tconst reported_ranges = new Set<string>();\n\n\t\tfunction report_onchange(node: AST.Attribute | ESTreeJSX.JSXAttribute) {\n\t\t\tconst range = node.range;\n\t\t\tif (!range) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst key = `${range[0]}:${range[1]}`;\n\t\t\tif (reported_ranges.has(key)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\treported_ranges.add(key);\n\n\t\t\tcontext.report({\n\t\t\t\tnode,\n\t\t\t\tmessageId: 'preferOnInput',\n\t\t\t\tfix(fixer) {\n\t\t\t\t\treturn fixer.replaceText(node.name, 'onInput');\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\treturn {\n\t\t\t// Check JSX attributes (standard JSX)\n\t\t\t'JSXAttribute[name.name=\"onChange\"]'(node: ESTreeJSX.JSXAttribute) {\n\t\t\t\treport_onchange(node);\n\t\t\t},\n\t\t\t// Check Attribute nodes (Ripple parser)\n\t\t\t'Attribute[name.name=\"onChange\"]'(node: AST.Attribute) {\n\t\t\t\treport_onchange(node);\n\t\t\t},\n\t\t\t// Check object properties (for spread props)\n\t\t\t'Property[key.name=\"onChange\"]'(node: AST.Property) {\n\t\t\t\t// Only report if this looks like it's in a props object\n\t\t\t\tconst ancestors = context.sourceCode.getAncestors(node);\n\t\t\t\tconst inObjectExpression = ancestors.some(\n\t\t\t\t\t(ancestor) => ancestor.type === 'ObjectExpression',\n\t\t\t\t);\n\n\t\t\t\tif (inObjectExpression) {\n\t\t\t\t\tcontext.report({\n\t\t\t\t\t\tnode,\n\t\t\t\t\t\tmessageId: 'preferOnInput',\n\t\t\t\t\t\tfix(fixer) {\n\t\t\t\t\t\t\treturn fixer.replaceText(node.key, 'onInput');\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t},\n};\n\nexport default rule;\n","import type { Rule } from 'eslint';\nimport type * as AST from '@tsrx/core/types/estree';\n\nconst rule: Rule.RuleModule = {\n\tmeta: {\n\t\ttype: 'problem',\n\t\tdocs: {\n\t\t\tdescription: 'Disallow return statements with JSX in Ripple components',\n\t\t\trecommended: true,\n\t\t},\n\t\tmessages: {\n\t\t\tnoReturn: 'Do not return JSX from Ripple components. Use JSX as statements instead.',\n\t\t},\n\t\tschema: [],\n\t},\n\tcreate(context) {\n\t\tlet insideComponent = 0;\n\n\t\treturn {\n\t\t\t// Track component boundaries\n\t\t\t\"ExpressionStatement > CallExpression[callee.name='component']\"() {\n\t\t\t\tinsideComponent++;\n\t\t\t},\n\t\t\t\"ExpressionStatement > CallExpression[callee.name='component']:exit\"() {\n\t\t\t\tinsideComponent--;\n\t\t\t},\n\t\t\t// Also track arrow functions and regular functions that might be components\n\t\t\t'VariableDeclarator[init.callee.name=\"component\"]'() {\n\t\t\t\tinsideComponent++;\n\t\t\t},\n\t\t\t'VariableDeclarator[init.callee.name=\"component\"]:exit'() {\n\t\t\t\tinsideComponent--;\n\t\t\t},\n\t\t\t// Check return statements\n\t\t\tReturnStatement(node: AST.ReturnStatement) {\n\t\t\t\tif (insideComponent > 0 && node.argument) {\n\t\t\t\t\t// Check if returning JSX (JSXElement, JSXFragment)\n\t\t\t\t\tif (node.argument.type === 'JSXElement' || node.argument.type === 'JSXFragment') {\n\t\t\t\t\t\tcontext.report({\n\t\t\t\t\t\t\tnode,\n\t\t\t\t\t\t\tmessageId: 'noReturn',\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t},\n};\n\nexport default rule;\n","import type { Rule } from 'eslint';\nimport type * as AST from '@tsrx/core/types/estree';\n\nconst rule: Rule.RuleModule = {\n\tmeta: {\n\t\ttype: 'problem',\n\t\tdocs: {\n\t\t\tdescription:\n\t\t\t\t'Require JSX in for...of loops within components, but disallow JSX in for...of loops within effects',\n\t\t\trecommended: true,\n\t\t},\n\t\tmessages: {\n\t\t\trequireJsxInLoop:\n\t\t\t\t'For...of loops in component bodies should contain JSX elements. Use JSX to render items.',\n\t\t\tnoJsxInEffectLoop:\n\t\t\t\t'For...of loops inside effect() should not contain JSX. Effects are for side effects, not rendering.',\n\t\t},\n\t\tschema: [],\n\t},\n\tcreate(context) {\n\t\tlet insideComponent = 0;\n\t\tlet insideEffect = 0;\n\n\t\tfunction containsJSX(node: AST.Node, visited: Set<AST.Node> = new Set()): boolean {\n\t\t\tif (!node) return false;\n\n\t\t\t// Avoid infinite loops from circular references\n\t\t\tif (visited.has(node)) return false;\n\t\t\tvisited.add(node);\n\n\t\t\t// Check if current node is JSX/Element (Ripple uses 'Element' type instead of 'JSXElement')\n\t\t\tif (\n\t\t\t\tnode.type === ('JSXElement' as string) ||\n\t\t\t\tnode.type === ('JSXFragment' as string) ||\n\t\t\t\tnode.type === ('Element' as string)\n\t\t\t) {\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tconst keys = Object.keys(node);\n\t\t\tfor (const key of keys) {\n\t\t\t\tif (key === 'parent' || key === 'loc' || key === 'range') {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst value = (node as any)[key];\n\t\t\t\tif (value && typeof value === 'object') {\n\t\t\t\t\tif (Array.isArray(value)) {\n\t\t\t\t\t\tfor (const item of value) {\n\t\t\t\t\t\t\tif (item && typeof item === 'object' && containsJSX(item, visited)) {\n\t\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (value.type && containsJSX(value, visited)) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn false;\n\t\t}\n\n\t\treturn {\n\t\t\tComponent() {\n\t\t\t\tinsideComponent++;\n\t\t\t},\n\t\t\t'Component:exit'() {\n\t\t\t\tinsideComponent--;\n\t\t\t},\n\n\t\t\t\"CallExpression[callee.name='effect']\"() {\n\t\t\t\tinsideEffect++;\n\t\t\t},\n\t\t\t\"CallExpression[callee.name='effect']:exit\"() {\n\t\t\t\tinsideEffect--;\n\t\t\t},\n\n\t\t\tForOfStatement(node: AST.ForOfStatement) {\n\t\t\t\tif (insideComponent === 0) return;\n\n\t\t\t\tconst hasJSX = containsJSX(node.body);\n\n\t\t\t\tif (insideEffect > 0) {\n\t\t\t\t\tif (hasJSX) {\n\t\t\t\t\t\tcontext.report({\n\t\t\t\t\t\t\tnode,\n\t\t\t\t\t\t\tmessageId: 'noJsxInEffectLoop',\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif (!hasJSX) {\n\t\t\t\t\t\tcontext.report({\n\t\t\t\t\t\t\tnode,\n\t\t\t\t\t\t\tmessageId: 'requireJsxInLoop',\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t},\n};\n\nexport default rule;\n","import type { Rule } from 'eslint';\n\nconst rule: Rule.RuleModule = {\n\tmeta: {\n\t\ttype: 'problem',\n\t\tdocs: {\n\t\t\tdescription: 'Disallow lazy destructuring (&[] / &{}) in TypeScript/JavaScript modules',\n\t\t\trecommended: true,\n\t\t},\n\t\tmessages: {\n\t\t\tnoLazyDestructuring:\n\t\t\t\t'Lazy destructuring (&[] / &{}) cannot be used in TypeScript/JavaScript modules. Use .value to read and write tracked values instead.',\n\t\t},\n\t\tschema: [],\n\t},\n\tcreate(context) {\n\t\tconst filename = context.filename;\n\n\t\t// Skip component files where lazy destructuring is valid\n\t\tif (filename && filename.endsWith('.tsrx')) {\n\t\t\treturn {};\n\t\t}\n\n\t\treturn {\n\t\t\tArrayPattern(node: any) {\n\t\t\t\tif (node.lazy === true) {\n\t\t\t\t\tcontext.report({\n\t\t\t\t\t\tnode,\n\t\t\t\t\t\tmessageId: 'noLazyDestructuring',\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t},\n\t\t\tObjectPattern(node: any) {\n\t\t\t\tif (node.lazy === true) {\n\t\t\t\t\tcontext.report({\n\t\t\t\t\t\tnode,\n\t\t\t\t\t\tmessageId: 'noLazyDestructuring',\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t},\n};\n\nexport default rule;\n","import type { Rule } from 'eslint';\nimport type * as AST from '@tsrx/core/types/estree';\nimport type { Scope } from 'eslint';\n\nconst rule: Rule.RuleModule = {\n\tmeta: {\n\t\ttype: 'problem',\n\t\tdocs: {\n\t\t\tdescription: 'Ensure variables used in for..of key expression are defined',\n\t\t\trecommended: true,\n\t\t},\n\t\tmessages: {\n\t\t\tundefinedVariable: \"Variable '{{name}}' is not defined.\",\n\t\t},\n\t\tschema: [],\n\t},\n\tcreate(context) {\n\t\treturn {\n\t\t\tForOfStatement(node: AST.ForOfStatement) {\n\t\t\t\tif (!node.key) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst checkIdentifier = (identifier: AST.Identifier) => {\n\t\t\t\t\tconst scope = context.sourceCode.getScope(node);\n\t\t\t\t\tconst variable = findVariable(scope, identifier.name);\n\n\t\t\t\t\tif (!variable) {\n\t\t\t\t\t\tcontext.report({\n\t\t\t\t\t\t\tnode: identifier,\n\t\t\t\t\t\t\tmessageId: 'undefinedVariable',\n\t\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\t\tname: identifier.name,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\tconst traverse = (node: AST.Node) => {\n\t\t\t\t\tif (!node) return;\n\n\t\t\t\t\tswitch (node.type) {\n\t\t\t\t\t\tcase 'Identifier':\n\t\t\t\t\t\t\tcheckIdentifier(node);\n\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase 'MemberExpression':\n\t\t\t\t\t\t\ttraverse(node.object);\n\n\t\t\t\t\t\t\tif (node.computed) {\n\t\t\t\t\t\t\t\ttraverse(node.property);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase 'BinaryExpression':\n\t\t\t\t\t\tcase 'LogicalExpression':\n\t\t\t\t\t\t\ttraverse(node.left);\n\t\t\t\t\t\t\ttraverse(node.right);\n\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase 'UnaryExpression':\n\t\t\t\t\t\t\ttraverse(node.argument);\n\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase 'CallExpression':\n\t\t\t\t\t\t\ttraverse(node.callee);\n\t\t\t\t\t\t\tnode.arguments.forEach(traverse);\n\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase 'ArrayExpression':\n\t\t\t\t\t\t\t(node.elements as (AST.Expression | AST.SpreadElement)[]).forEach(traverse);\n\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase 'ObjectExpression':\n\t\t\t\t\t\t\tnode.properties.forEach((prop: AST.Property | AST.SpreadElement) => {\n\t\t\t\t\t\t\t\tif (prop.type === 'Property') {\n\t\t\t\t\t\t\t\t\tif (prop.computed) {\n\t\t\t\t\t\t\t\t\t\ttraverse(prop.key);\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\ttraverse(prop.value);\n\t\t\t\t\t\t\t\t} else if (prop.type === 'SpreadElement') {\n\t\t\t\t\t\t\t\t\ttraverse(prop.argument);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase 'ConditionalExpression':\n\t\t\t\t\t\t\ttraverse(node.test);\n\t\t\t\t\t\t\ttraverse(node.consequent);\n\t\t\t\t\t\t\ttraverse(node.alternate);\n\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase 'TemplateLiteral':\n\t\t\t\t\t\t\tnode.expressions.forEach(traverse);\n\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\ttraverse(node.key);\n\t\t\t},\n\t\t};\n\t},\n};\n\nfunction findVariable(scope: Scope.Scope, name: string) {\n\tlet currentScope: Scope.Scope | null = scope;\n\n\twhile (currentScope) {\n\t\tconst variable = currentScope.variables.find((v: { name: string }) => v.name === name);\n\n\t\tif (variable) {\n\t\t\treturn variable;\n\t\t}\n\n\t\t// Also check references for global variables or variables defined in the loop itself (like the loop variable)\n\t\t// The loop variable might not be in the 'variables' list of the surrounding scope yet if we are inside the loop statement\n\t\t// But for 'for-of', the loop variable is in a special scope or the upper scope.\n\t\t// Let's rely on standard scope analysis.\n\n\t\t// Special case: check if the variable is the loop variable itself (left side of for-of)\n\t\t// The scope analysis might handle this, but let's be sure.\n\n\t\tcurrentScope = currentScope.upper;\n\t}\n\n\t// If not found in scopes, it might be a global variable.\n\t// ESLint scope analysis usually includes globals in the global scope.\n\t// However, if we are in a module, we might need to check if it's an implicit global or defined elsewhere.\n\t// For now, let's assume standard scope resolution works.\n\n\treturn null;\n}\n\nexport default rule;\n","import { createRequire } from 'module';\nimport noModuleScopeTrack from './rules/no-module-scope-track.js';\nimport preferOnInput from './rules/prefer-oninput.js';\nimport noReturnInComponent from './rules/no-return-in-component.js';\nimport controlFlowJsx from './rules/control-flow-jsx.js';\nimport noLazyDestructuringInModules from './rules/no-lazy-destructuring-in-modules.js';\nimport validForOfKey from './rules/valid-for-of-key.js';\n\nconst plugin = {\n\tmeta: {\n\t\tname: '@tsrx/eslint-plugin',\n\t\tversion: '0.1.3',\n\t},\n\trules: {\n\t\t'no-module-scope-track': noModuleScopeTrack,\n\t\t'prefer-oninput': preferOnInput,\n\t\t'no-return-in-component': noReturnInComponent,\n\t\t'control-flow-jsx': controlFlowJsx,\n\t\t'no-lazy-destructuring-in-modules': noLazyDestructuringInModules,\n\t\t'valid-for-of-key': validForOfKey,\n\t},\n\tconfigs: {} as any,\n};\n\n// Try to load optional parsers\nconst require = createRequire(import.meta.url);\n\nlet rippleParser: any;\nlet tsParser: any;\n\ntry {\n\trippleParser = require('@tsrx/eslint-parser');\n} catch {\n\t// @tsrx/eslint-parser is optional\n\trippleParser = null;\n}\n\ntry {\n\ttsParser = require('@typescript-eslint/parser');\n} catch {\n\t// @typescript-eslint/parser is optional\n\ttsParser = null;\n}\n\n// Helper to create config objects\nfunction createConfig(name: string, files: string[], parser: any) {\n\tconst config: any = {\n\t\tname,\n\t\tfiles,\n\t\tplugins: {\n\t\t\tripple: plugin,\n\t\t},\n\t\trules: {\n\t\t\t'ripple/no-module-scope-track': 'error',\n\t\t\t'ripple/prefer-oninput': 'warn',\n\t\t\t'ripple/no-return-in-component': 'error',\n\t\t\t'ripple/control-flow-jsx': 'error',\n\t\t\t'ripple/no-lazy-destructuring-in-modules': 'error',\n\t\t\t'ripple/valid-for-of-key': 'error',\n\t\t},\n\t};\n\n\t// Only add parser if it's available\n\tif (parser) {\n\t\tconfig.languageOptions = {\n\t\t\tparser,\n\t\t\tparserOptions: {\n\t\t\t\tecmaVersion: 'latest',\n\t\t\t\tsourceType: 'module',\n\t\t\t},\n\t\t};\n\t}\n\n\treturn config;\n}\n\n// Recommended configuration (flat config format)\nplugin.configs.recommended = [\n\tcreateConfig('ripple/recommended-ripple-files', ['**/*.tsrx'], rippleParser),\n\tcreateConfig('ripple/recommended-typescript-files', ['**/*.ts', '**/*.tsx'], tsParser),\n\t{\n\t\tname: 'ripple/ignores',\n\t\tignores: ['**/*.d.ts', '**/node_modules/**', '**/dist/**', '**/build/**'],\n\t},\n];\n\n// Strict configuration (flat config format)\nplugin.configs.strict = [\n\tcreateConfig('ripple/strict-ripple-files', ['**/*.tsrx'], rippleParser),\n\tcreateConfig('ripple/strict-typescript-files', ['**/*.ts', '**/*.tsx'], tsParser),\n\t{\n\t\tname: 'ripple/ignores',\n\t\tignores: ['**/*.d.ts', '**/node_modules/**', '**/dist/**', '**/build/**'],\n\t},\n];\n\nexport default plugin;\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@tsrx/eslint-plugin",
3
+ "version": "0.3.25",
4
+ "description": "ESLint plugin for Ripple",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "exports": {
8
+ ".": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "keywords": [
14
+ "eslint",
15
+ "eslintplugin",
16
+ "eslint-plugin",
17
+ "ripple"
18
+ ],
19
+ "author": "Dominic Gannaway",
20
+ "license": "MIT",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/Ripple-TS/ripple.git",
24
+ "directory": "packages/eslint-plugin"
25
+ },
26
+ "bugs": {
27
+ "url": "https://github.com/Ripple-TS/ripple/issues"
28
+ },
29
+ "homepage": "https://ripple-ts.com",
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "peerDependencies": {
34
+ "eslint": ">=9.0.0",
35
+ "@typescript-eslint/parser": "^8.56.1",
36
+ "@tsrx/eslint-parser": "0.3.25"
37
+ },
38
+ "devDependencies": {
39
+ "@types/eslint": "^9.6.1",
40
+ "@types/node": "^24.3.0",
41
+ "@typescript-eslint/parser": "^8.56.1",
42
+ "eslint": "^10.2.1",
43
+ "tsdown": "^0.20.3",
44
+ "typescript": "^5.9.3",
45
+ "vitest": "^4.1.4",
46
+ "@tsrx/core": "0.0.5",
47
+ "@tsrx/eslint-parser": "0.3.25"
48
+ },
49
+ "engines": {
50
+ "node": ">=22.0.0"
51
+ },
52
+ "scripts": {
53
+ "dist": "perl -pi -e 's/\"main\": \"src\\/index.ts\"/\"main\": \"dist\\/index.js\"/' package.json",
54
+ "src": "perl -pi -e 's/\"main\": \"dist\\/index.js\"/\"main\": \"src\\/index.ts\"/' package.json",
55
+ "build": "pnpm dist && tsdown && pnpm src || pnpm src",
56
+ "test": "vitest run",
57
+ "test:watch": "vitest"
58
+ }
59
+ }
package/src/index.ts ADDED
@@ -0,0 +1,97 @@
1
+ import { createRequire } from 'module';
2
+ import noModuleScopeTrack from './rules/no-module-scope-track.js';
3
+ import preferOnInput from './rules/prefer-oninput.js';
4
+ import noReturnInComponent from './rules/no-return-in-component.js';
5
+ import controlFlowJsx from './rules/control-flow-jsx.js';
6
+ import noLazyDestructuringInModules from './rules/no-lazy-destructuring-in-modules.js';
7
+ import validForOfKey from './rules/valid-for-of-key.js';
8
+
9
+ const plugin = {
10
+ meta: {
11
+ name: '@tsrx/eslint-plugin',
12
+ version: '0.1.3',
13
+ },
14
+ rules: {
15
+ 'no-module-scope-track': noModuleScopeTrack,
16
+ 'prefer-oninput': preferOnInput,
17
+ 'no-return-in-component': noReturnInComponent,
18
+ 'control-flow-jsx': controlFlowJsx,
19
+ 'no-lazy-destructuring-in-modules': noLazyDestructuringInModules,
20
+ 'valid-for-of-key': validForOfKey,
21
+ },
22
+ configs: {} as any,
23
+ };
24
+
25
+ // Try to load optional parsers
26
+ const require = createRequire(import.meta.url);
27
+
28
+ let rippleParser: any;
29
+ let tsParser: any;
30
+
31
+ try {
32
+ rippleParser = require('@tsrx/eslint-parser');
33
+ } catch {
34
+ // @tsrx/eslint-parser is optional
35
+ rippleParser = null;
36
+ }
37
+
38
+ try {
39
+ tsParser = require('@typescript-eslint/parser');
40
+ } catch {
41
+ // @typescript-eslint/parser is optional
42
+ tsParser = null;
43
+ }
44
+
45
+ // Helper to create config objects
46
+ function createConfig(name: string, files: string[], parser: any) {
47
+ const config: any = {
48
+ name,
49
+ files,
50
+ plugins: {
51
+ ripple: plugin,
52
+ },
53
+ rules: {
54
+ 'ripple/no-module-scope-track': 'error',
55
+ 'ripple/prefer-oninput': 'warn',
56
+ 'ripple/no-return-in-component': 'error',
57
+ 'ripple/control-flow-jsx': 'error',
58
+ 'ripple/no-lazy-destructuring-in-modules': 'error',
59
+ 'ripple/valid-for-of-key': 'error',
60
+ },
61
+ };
62
+
63
+ // Only add parser if it's available
64
+ if (parser) {
65
+ config.languageOptions = {
66
+ parser,
67
+ parserOptions: {
68
+ ecmaVersion: 'latest',
69
+ sourceType: 'module',
70
+ },
71
+ };
72
+ }
73
+
74
+ return config;
75
+ }
76
+
77
+ // Recommended configuration (flat config format)
78
+ plugin.configs.recommended = [
79
+ createConfig('ripple/recommended-ripple-files', ['**/*.tsrx'], rippleParser),
80
+ createConfig('ripple/recommended-typescript-files', ['**/*.ts', '**/*.tsx'], tsParser),
81
+ {
82
+ name: 'ripple/ignores',
83
+ ignores: ['**/*.d.ts', '**/node_modules/**', '**/dist/**', '**/build/**'],
84
+ },
85
+ ];
86
+
87
+ // Strict configuration (flat config format)
88
+ plugin.configs.strict = [
89
+ createConfig('ripple/strict-ripple-files', ['**/*.tsrx'], rippleParser),
90
+ createConfig('ripple/strict-typescript-files', ['**/*.ts', '**/*.tsx'], tsParser),
91
+ {
92
+ name: 'ripple/ignores',
93
+ ignores: ['**/*.d.ts', '**/node_modules/**', '**/dist/**', '**/build/**'],
94
+ },
95
+ ];
96
+
97
+ export default plugin;