@murky-web/oxlint-plugin-solid 0.0.1

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/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # @murky-web/oxlint-plugin-solid
2
+
3
+ Workspace-Paket fuer den lokalen Oxlint-JS-Plugin-Port der Solid-Regeln.
4
+
5
+ Es enthaelt die Solid-Regeln lokal im Paket und erweitert den Upstream-Satz um
6
+ projektspezifische Regeln wie `solid/prefer-arrow-components`.
7
+
8
+ Die Regelmodule unter `src/rules/` sind aus dem Upstream-Quellstand abgeleitet
9
+ und laufen ohne `eslint-plugin-solid` als Zielprojekt-Dependency.
10
+
11
+ Aktuell sind enthalten:
12
+
13
+ - die komplette von `eslint-plugin-solid` exportierte Regelmenge
14
+ - die zusaetzliche Projektregel `solid/prefer-arrow-components`
15
+ - ein Test-Harness, der die exportierte Rule-Surface und echte Diagnostik
16
+ gegen Temp-Projekte prueft
17
+
18
+ Aktuell wird das Paket nicht direkt im Zielprojekt installiert. Stattdessen
19
+ kopiert `@murky-web/config` die Rule-Runtime in `./oxc/jsplugins/solid/`, damit
20
+ Oxlint sie ueber einen lokalen `jsPlugins`-Pfad laden kann.
21
+
22
+ Wichtig dabei:
23
+
24
+ - `@murky-web/config` loest dieses Paket als normale Workspace-Dependency auf
25
+ - der Installer kopiert den lokalen Plugin-Src-Ordner aus der installierten
26
+ Paketauflösung
27
+ - Zielprojekte brauchen dadurch weiter nur die kopierte Runtime unter
28
+ `./oxc/jsplugins/solid/`, nicht dieses Paket als direkte Dependency
29
+
30
+ ## Nutzung ueber @murky-web/config
31
+
32
+ Im Zielprojekt:
33
+
34
+ ```bash
35
+ web-dev-config init --oxc --typescript --frontend-solid
36
+ ```
37
+
38
+ Danach:
39
+
40
+ - liegt die lokale Rule-Runtime unter `./oxc/jsplugins/solid/`
41
+ - wird `./linting/solid.jsonc` in die Oxc-Konfiguration eingehängt
42
+ - zeigt `jsPlugins` auf `./jsplugins/solid/index.mjs`
43
+ - feuern sowohl die portierten Solid-Regeln als auch
44
+ `solid/prefer-arrow-components`
45
+
46
+ Ein typischer Fix-Fall:
47
+
48
+ ```tsx
49
+ export function Card(props: Props) {
50
+ return <section>{props.children}</section>;
51
+ }
52
+ ```
53
+
54
+ wird mit `oxlint --fix` zu:
55
+
56
+ ```tsx
57
+ import type { ParentComponent } from "solid-js";
58
+
59
+ export const Card: ParentComponent<Props> = (props) => {
60
+ return <section>{props.children}</section>;
61
+ };
62
+ ```
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@murky-web/oxlint-plugin-solid",
3
+ "version": "0.0.1",
4
+ "description": "Local Oxlint JS plugin port for Solid rules plus Murky-specific Solid conventions.",
5
+ "license": "MIT",
6
+ "private": false,
7
+ "type": "module",
8
+ "exports": {
9
+ ".": "./src/index.mjs"
10
+ },
11
+ "files": [
12
+ "README.md",
13
+ "src"
14
+ ],
15
+ "scripts": {
16
+ "test": "bun test ./tests"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/MurkyTheMurloc/web_dev_tools.git",
21
+ "directory": "packages/oxlint-plugin-solid"
22
+ },
23
+ "homepage": "https://github.com/MurkyTheMurloc/web_dev_tools/tree/main/packages/oxlint-plugin-solid",
24
+ "bugs": {
25
+ "url": "https://github.com/MurkyTheMurloc/web_dev_tools/issues"
26
+ },
27
+ "sideEffects": false,
28
+ "dependencies": {
29
+ "@typescript-eslint/utils": "^8.57.1",
30
+ "eslint": "^9.38.0",
31
+ "estraverse": "^5.3.0",
32
+ "is-html": "^2.0.0",
33
+ "kebab-case": "^1.0.2",
34
+ "known-css-properties": "^0.30.0",
35
+ "style-to-object": "^1.0.8"
36
+ },
37
+ "devDependencies": {
38
+ "solid-js": "^1.9.11"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public",
42
+ "provenance": true
43
+ }
44
+ }
package/src/compat.mjs ADDED
@@ -0,0 +1,53 @@
1
+ import { ASTUtils } from "@typescript-eslint/utils";
2
+
3
+ export function getSourceCode(context) {
4
+ if (typeof context.getSourceCode === "function") {
5
+ return context.getSourceCode();
6
+ }
7
+
8
+ return context.sourceCode;
9
+ }
10
+
11
+ export function getScope(context, node) {
12
+ const sourceCode = getSourceCode(context);
13
+
14
+ if (typeof sourceCode.getScope === "function") {
15
+ return sourceCode.getScope(node);
16
+ }
17
+
18
+ if (typeof context.getScope === "function") {
19
+ return context.getScope();
20
+ }
21
+
22
+ return context.sourceCode.getScope(node);
23
+ }
24
+
25
+ export function findVariable(context, node) {
26
+ return ASTUtils.findVariable(getScope(context, node), node);
27
+ }
28
+
29
+ export function markVariableAsUsed(context, name, node) {
30
+ if (typeof context.markVariableAsUsed === "function") {
31
+ context.markVariableAsUsed(name);
32
+ return;
33
+ }
34
+
35
+ const sourceCode = getSourceCode(context);
36
+ if (typeof sourceCode.markVariableAsUsed !== "function") {
37
+ return;
38
+ }
39
+
40
+ try {
41
+ sourceCode.markVariableAsUsed(name, node);
42
+ } catch (error) {
43
+ if (
44
+ error instanceof Error &&
45
+ error.message.includes("markVariableAsUsed") &&
46
+ error.message.includes("not implemented")
47
+ ) {
48
+ return;
49
+ }
50
+
51
+ throw error;
52
+ }
53
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,56 @@
1
+ import componentsReturnOnceRule from "./rules/components_return_once.mjs";
2
+ import eventHandlersRule from "./rules/event_handlers.mjs";
3
+ import importsRule from "./rules/imports.mjs";
4
+ import jsxNoDuplicatePropsRule from "./rules/jsx_no_duplicate_props.mjs";
5
+ import { jsxNoScriptUrlRule } from "./rules/jsx_no_script_url.mjs";
6
+ import jsxNoUndefRule from "./rules/jsx_no_undef.mjs";
7
+ import { jsxUsesVarsRule } from "./rules/jsx_uses_vars.mjs";
8
+ import { noArrayHandlersRule } from "./rules/no_array_handlers.mjs";
9
+ import noDestructureRule from "./rules/no_destructure.mjs";
10
+ import noInnerhtmlRule from "./rules/no_innerhtml.mjs";
11
+ import noProxyApisRule from "./rules/no_proxy_apis.mjs";
12
+ import noReactDepsRule from "./rules/no_react_deps.mjs";
13
+ import { noReactSpecificPropsRule } from "./rules/no_react_specific_props.mjs";
14
+ import noUnknownNamespacesRule from "./rules/no_unknown_namespaces.mjs";
15
+ import { preferArrowComponentsRule } from "./rules/prefer_arrow_components.mjs";
16
+ import preferClasslistRule from "./rules/prefer_classlist.mjs";
17
+ import preferForRule from "./rules/prefer_for.mjs";
18
+ import preferShowRule from "./rules/prefer_show.mjs";
19
+ import reactivityRule from "./rules/reactivity.mjs";
20
+ import selfClosingCompRule from "./rules/self_closing_comp.mjs";
21
+ import stylePropRule from "./rules/style_prop.mjs";
22
+ import validateJsxNestingRule from "./rules/validate_jsx_nesting.mjs";
23
+
24
+ const extendedPlugin = {
25
+ meta: {
26
+ name: "solid",
27
+ version: "0.14.5",
28
+ },
29
+ rules: {
30
+ "components-return-once": componentsReturnOnceRule,
31
+ "event-handlers": eventHandlersRule,
32
+ imports: importsRule,
33
+ "jsx-no-duplicate-props": jsxNoDuplicatePropsRule,
34
+ "jsx-no-script-url": jsxNoScriptUrlRule,
35
+ "jsx-no-undef": jsxNoUndefRule,
36
+ "jsx-uses-vars": jsxUsesVarsRule,
37
+ "no-array-handlers": noArrayHandlersRule,
38
+ "no-destructure": noDestructureRule,
39
+ "no-innerhtml": noInnerhtmlRule,
40
+ "no-proxy-apis": noProxyApisRule,
41
+ "no-react-deps": noReactDepsRule,
42
+ "no-react-specific-props": noReactSpecificPropsRule,
43
+ "no-unknown-namespaces": noUnknownNamespacesRule,
44
+ "prefer-arrow-components": preferArrowComponentsRule,
45
+ "prefer-classlist": preferClasslistRule,
46
+ "prefer-for": preferForRule,
47
+ "prefer-show": preferShowRule,
48
+ reactivity: reactivityRule,
49
+ "self-closing-comp": selfClosingCompRule,
50
+ "style-prop": stylePropRule,
51
+ "validate-jsx-nesting": validateJsxNestingRule,
52
+ },
53
+ };
54
+
55
+ // oxlint-disable-next-line import/no-default-export -- Oxlint JS plugins require a default export surface.
56
+ export default extendedPlugin;
@@ -0,0 +1,202 @@
1
+ import { ESLintUtils } from "@typescript-eslint/utils";
2
+
3
+ import { getSourceCode } from "../compat.mjs";
4
+ import { getFunctionName } from "../utils.mjs";
5
+ const createRule = ESLintUtils.RuleCreator.withoutDocs;
6
+ const isNothing = (node) => {
7
+ if (!node) {
8
+ return true;
9
+ }
10
+ switch (node.type) {
11
+ case "Literal":
12
+ return [null, undefined, false, ""].includes(node.value);
13
+ case "JSXFragment":
14
+ return !node.children || node.children.every(isNothing);
15
+ default:
16
+ return false;
17
+ }
18
+ };
19
+ const getLineLength = (loc) => loc.end.line - loc.start.line + 1;
20
+ export default createRule({
21
+ meta: {
22
+ type: "problem",
23
+ docs: {
24
+ description:
25
+ "Disallow early returns in components. Solid components only run once, and so conditionals should be inside JSX.",
26
+ url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/components-return-once.md",
27
+ },
28
+ fixable: "code",
29
+ schema: [],
30
+ messages: {
31
+ noEarlyReturn:
32
+ "Solid components run once, so an early return breaks reactivity. Move the condition inside a JSX element, such as a fragment or <Show />.",
33
+ noConditionalReturn:
34
+ "Solid components run once, so a conditional return breaks reactivity. Move the condition inside a JSX element, such as a fragment or <Show />.",
35
+ },
36
+ },
37
+ defaultOptions: [],
38
+ create(context) {
39
+ const functionStack = [];
40
+ const putIntoJSX = (node) => {
41
+ const text = getSourceCode(context).getText(node);
42
+ return node.type === "JSXElement" || node.type === "JSXFragment"
43
+ ? text
44
+ : `{${text}}`;
45
+ };
46
+ const currentFunction = () => functionStack[functionStack.length - 1];
47
+ const onFunctionEnter = (node) => {
48
+ let lastReturn;
49
+ if (node.body.type === "BlockStatement") {
50
+ // find last statement, ignoring function/class/variable declarations (hoisting)
51
+ const last = node.body.body.findLast(
52
+ (node) => !node.type.endsWith("Declaration"),
53
+ );
54
+ // if it's a return, store it
55
+ if (last && last.type === "ReturnStatement") {
56
+ lastReturn = last;
57
+ }
58
+ }
59
+ functionStack.push({
60
+ isComponent: false,
61
+ lastReturn,
62
+ earlyReturns: [],
63
+ });
64
+ };
65
+ const onFunctionExit = (node) => {
66
+ if (
67
+ // "render props" aren't components
68
+ getFunctionName(node)?.match(/^[a-z]/) ||
69
+ node.parent?.type === "JSXExpressionContainer" ||
70
+ // ignore createMemo(() => conditional JSX), report HOC(() => conditional JSX)
71
+ (node.parent?.type === "CallExpression" &&
72
+ node.parent.arguments.some((n) => n === node) &&
73
+ !node.parent.callee.name?.match(/^[A-Z]/))
74
+ ) {
75
+ currentFunction().isComponent = false;
76
+ }
77
+ if (currentFunction().isComponent) {
78
+ // Warn on each early return
79
+ currentFunction().earlyReturns.forEach((earlyReturn) => {
80
+ context.report({
81
+ node: earlyReturn,
82
+ messageId: "noEarlyReturn",
83
+ });
84
+ });
85
+ const argument = currentFunction().lastReturn?.argument;
86
+ if (argument?.type === "ConditionalExpression") {
87
+ const sourceCode = getSourceCode(context);
88
+ context.report({
89
+ node: argument.parent,
90
+ messageId: "noConditionalReturn",
91
+ fix: (fixer) => {
92
+ const { test, consequent, alternate } = argument;
93
+ const conditions = [{ test, consequent }];
94
+ let fallback = alternate;
95
+ while (fallback.type === "ConditionalExpression") {
96
+ conditions.push({
97
+ test: fallback.test,
98
+ consequent: fallback.consequent,
99
+ });
100
+ fallback = fallback.alternate;
101
+ }
102
+ if (conditions.length >= 2) {
103
+ // we have a nested ternary, use <Switch><Match /></Switch>
104
+ const fallbackStr = !isNothing(fallback)
105
+ ? ` fallback={${sourceCode.getText(fallback)}}`
106
+ : "";
107
+ return fixer.replaceText(
108
+ argument,
109
+ `<Switch${fallbackStr}>\n${conditions
110
+ .map(
111
+ ({ test, consequent }) =>
112
+ `<Match when={${sourceCode.getText(test)}}>${putIntoJSX(consequent)}</Match>`,
113
+ )
114
+ .join("\n")}\n</Switch>`,
115
+ );
116
+ }
117
+ if (isNothing(consequent)) {
118
+ // we have a single ternary and the consequent is nothing. Negate the condition and use a <Show>.
119
+ return fixer.replaceText(
120
+ argument,
121
+ `<Show when={!(${sourceCode.getText(test)})}>${putIntoJSX(alternate)}</Show>`,
122
+ );
123
+ }
124
+ if (
125
+ isNothing(fallback) ||
126
+ getLineLength(consequent.loc) >=
127
+ getLineLength(alternate.loc) * 1.5
128
+ ) {
129
+ // we have a standard ternary, and the alternate is a bit shorter in LOC than the consequent, which
130
+ // should be enough to tell that it's logically a fallback instead of an equal branch.
131
+ const fallbackStr = !isNothing(fallback)
132
+ ? ` fallback={${sourceCode.getText(fallback)}}`
133
+ : "";
134
+ return fixer.replaceText(
135
+ argument,
136
+ `<Show when={${sourceCode.getText(test)}}${fallbackStr}>${putIntoJSX(consequent)}</Show>`,
137
+ );
138
+ }
139
+ // we have a standard ternary, but no signal from the user as to which branch is the "fallback" and
140
+ // which is the children. Move the whole conditional inside a JSX fragment.
141
+ return fixer.replaceText(
142
+ argument,
143
+ `<>${putIntoJSX(argument)}</>`,
144
+ );
145
+ },
146
+ });
147
+ } else if (argument?.type === "LogicalExpression") {
148
+ if (argument.operator === "&&") {
149
+ const sourceCode = getSourceCode(context);
150
+ // we have a `return condition && expression`--put that in a <Show />
151
+ context.report({
152
+ node: argument,
153
+ messageId: "noConditionalReturn",
154
+ fix: (fixer) => {
155
+ const { left: test, right: consequent } =
156
+ argument;
157
+ return fixer.replaceText(
158
+ argument,
159
+ `<Show when={${sourceCode.getText(test)}}>${putIntoJSX(consequent)}</Show>`,
160
+ );
161
+ },
162
+ });
163
+ } else {
164
+ // we have some other kind of conditional, warn
165
+ context.report({
166
+ node: argument,
167
+ messageId: "noConditionalReturn",
168
+ });
169
+ }
170
+ }
171
+ }
172
+ // Pop on exit
173
+ functionStack.pop();
174
+ };
175
+ return {
176
+ FunctionDeclaration: onFunctionEnter,
177
+ FunctionExpression: onFunctionEnter,
178
+ ArrowFunctionExpression: onFunctionEnter,
179
+ "FunctionDeclaration:exit": onFunctionExit,
180
+ "FunctionExpression:exit": onFunctionExit,
181
+ "ArrowFunctionExpression:exit": onFunctionExit,
182
+ JSXElement() {
183
+ if (functionStack.length) {
184
+ currentFunction().isComponent = true;
185
+ }
186
+ },
187
+ JSXFragment() {
188
+ if (functionStack.length) {
189
+ currentFunction().isComponent = true;
190
+ }
191
+ },
192
+ ReturnStatement(node) {
193
+ if (
194
+ functionStack.length &&
195
+ node !== currentFunction().lastReturn
196
+ ) {
197
+ currentFunction().earlyReturns.push(node);
198
+ }
199
+ },
200
+ };
201
+ },
202
+ });