Skip to content

Commit 0e1dc25

Browse files
committed
Create initial linting plugin for oxlint/eslint
1 parent 93d71f6 commit 0e1dc25

23 files changed

+3048
-3
lines changed

.changeset/khaki-bears-divide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@preact/eslint-plugin-signals": minor
3+
---
4+
5+
Initial release

docs/.oxlintrc.json

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"$schema": "./node_modules/oxlint/configuration_schema.json",
3+
"plugins": null,
4+
"jsPlugins": ["eslint-plugin-signals"],
5+
"categories": {},
6+
"rules": {
7+
"eslint-plugin-signals/no-signal-write-in-computed": "error",
8+
"eslint-plugin-signals/no-value-after-await": "error",
9+
"eslint-plugin-signals/no-signal-truthiness": "error",
10+
"eslint-plugin-signals/no-signal-in-component-body": "error",
11+
"eslint-plugin-signals/no-conditional-value-read": "warn",
12+
"eslint/no-unused-vars": "off",
13+
"eslint/no-useless-escape": "off",
14+
"eslint-plugin-unicorn/no-empty-file": "off"
15+
},
16+
"settings": {
17+
"jsx-a11y": {
18+
"polymorphicPropName": null,
19+
"components": {},
20+
"attributes": {}
21+
},
22+
"next": {
23+
"rootDir": []
24+
},
25+
"react": {
26+
"formComponents": [],
27+
"linkComponents": [],
28+
"version": null,
29+
"componentWrapperFunctions": []
30+
},
31+
"jsdoc": {
32+
"ignorePrivate": false,
33+
"ignoreInternal": false,
34+
"ignoreReplacesDocs": true,
35+
"overrideReplacesDocs": true,
36+
"augmentsExtendsReplacesDocs": false,
37+
"implementsReplacesDocs": false,
38+
"exemptDestructuredRootsFromChecks": false,
39+
"tagNamePreference": {}
40+
},
41+
"vitest": {
42+
"typecheck": false
43+
}
44+
},
45+
"env": {
46+
"builtin": true
47+
},
48+
"globals": {},
49+
"ignorePatterns": []
50+
}

docs/demos/bench.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,9 @@ function createRunner() {
232232
results[i].value = result;
233233
await sleep(100);
234234
}
235+
235236
running.value = false;
236-
console.log(`Finished in ${total.value.toFixed(2)}ms`);
237+
console.log(`Finished in ${total.peek().toFixed(2)}ms`);
237238
}
238239
return { run, running, results, total };
239240
}

docs/demos/linting/index.tsx

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import {
2+
useModel,
3+
useSignal,
4+
useComputed,
5+
useSignalEffect,
6+
} from "@preact/signals";
7+
import { signal, computed, effect, untracked } from "@preact/signals-core";
8+
import { Model } from "./model";
9+
10+
export const Linting = () => {
11+
const sig = useSignal(0);
12+
const model = useModel(Model);
13+
14+
signal(0);
15+
effect(() => {});
16+
computed(() => {});
17+
18+
// @ts-ignore - we are not using bar, just want to test the linting
19+
const bar = useComputed(() => {
20+
// Both should complain about these
21+
model.count.value = 5;
22+
sig.value = 6;
23+
return "foo";
24+
});
25+
26+
useSignalEffect(() => {
27+
const perform = async () => {
28+
await new Promise(resolve => setTimeout(resolve, 100));
29+
// Both should complain about these
30+
// @ts-ignore - we are not using count, just want to test the linting
31+
const count = model.count.value;
32+
// @ts-ignore - we are not using sigValue, just want to test the linting
33+
const sigValue = sig.value;
34+
35+
// Should not complain about this
36+
sig.value++;
37+
// Should not complain about this
38+
sig.value = 10;
39+
};
40+
41+
perform();
42+
});
43+
44+
// Issue #621: .value after a non-reactive guard (.peek / plain variable)
45+
// should warn because the signal won't be tracked as a dependency.
46+
useSignalEffect(() => {
47+
const id = model.count.peek();
48+
if (!id) return;
49+
// Both should complain about this — .value after non-reactive guard
50+
console.log(sig.value);
51+
});
52+
53+
// .value after a .value guard is OK — the guard signal IS tracked
54+
useSignalEffect(() => {
55+
if (!model.count.value) return;
56+
// Should NOT complain — model.count is tracked by the guard above
57+
console.log(sig.value);
58+
});
59+
60+
// untracked() guard — .value inside untracked is non-reactive, like .peek()
61+
useSignalEffect(() => {
62+
if (!untracked(() => model.count.value)) return;
63+
// Should complain — untracked guard is non-reactive
64+
console.log(sig.value);
65+
});
66+
67+
// ESLint should complain about this, oxlint not
68+
if (model.count) return <p>Lint error</p>;
69+
// Both should complain about this
70+
if (sig) return <p>Lint error</p>;
71+
};

docs/demos/linting/model.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { createModel, signal } from "@preact/signals-core";
2+
3+
export const Model = createModel(() => {
4+
const count = signal(0);
5+
6+
return { count };
7+
});

docs/demos/render-flasher.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ options.diffed = (vnode: VNode) => {
6868
);
6969
};
7070

71-
// eslint-disable-next-line @typescript-eslint/ban-types
7271
function getName(type: string | (Function & { displayName?: string })) {
7372
if (typeof type === "string") return type;
7473
return (type && (type.name || type.displayName)) || String(type);

docs/eslint.config.mjs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import tsParser from "@typescript-eslint/parser";
2+
import signals from "eslint-plugin-signals";
3+
4+
export default [
5+
{
6+
files: ["**/*.{ts,tsx}"],
7+
languageOptions: {
8+
ecmaVersion: 2022,
9+
sourceType: "module",
10+
parser: tsParser,
11+
parserOptions: {
12+
ecmaFeatures: { jsx: true },
13+
project: true,
14+
tsconfigRootDir: import.meta.dirname,
15+
},
16+
},
17+
plugins: { signals },
18+
rules: {
19+
"signals/no-signal-write-in-computed": "error",
20+
"signals/no-value-after-await": "error",
21+
"signals/no-signal-truthiness": "error",
22+
"signals/no-conditional-value-read": "warn",
23+
},
24+
},
25+
];

docs/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
"scripts": {
55
"start": "vite",
66
"build": "vite build",
7-
"preview": "vite preview"
7+
"preview": "vite preview",
8+
"oxlint": "oxlint",
9+
"eslint": "eslint"
810
},
911
"postcss": {
1012
"plugins": {
@@ -26,9 +28,13 @@
2628
},
2729
"devDependencies": {
2830
"@babel/core": "^7.28.4",
31+
"@typescript-eslint/parser": "^8.0.0",
2932
"@preact/preset-vite": "^2.3.0",
3033
"@types/react": "^18.0.18",
3134
"@types/react-dom": "^18.0.6",
35+
"eslint": "^9.0.0",
36+
"eslint-plugin-signals": "workspace:../packages/eslint-plugin-signals",
37+
"oxlint": "^1.47.0",
3238
"postcss": "^8.4.31",
3339
"postcss-nesting": "^10.1.10",
3440
"tiny-glob": "^0.2.9",

0 commit comments

Comments
 (0)