@trackunit/eslint-plugin-trackunit 0.0.2
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/CHANGELOG.md +9 -0
- package/README.md +117 -0
- package/package.json +31 -0
- package/src/index.d.ts +8 -0
- package/src/index.js +20 -0
- package/src/index.js.map +1 -0
- package/src/lib/config/fragments/ignores.d.ts +2 -0
- package/src/lib/config/fragments/ignores.js +18 -0
- package/src/lib/config/fragments/ignores.js.map +1 -0
- package/src/lib/config/fragments/import-rules.d.ts +3 -0
- package/src/lib/config/fragments/import-rules.js +58 -0
- package/src/lib/config/fragments/import-rules.js.map +1 -0
- package/src/lib/config/fragments/jest-overrides.d.ts +2 -0
- package/src/lib/config/fragments/jest-overrides.js +30 -0
- package/src/lib/config/fragments/jest-overrides.js.map +1 -0
- package/src/lib/config/fragments/jsdoc-rules.d.ts +3 -0
- package/src/lib/config/fragments/jsdoc-rules.js +71 -0
- package/src/lib/config/fragments/jsdoc-rules.js.map +1 -0
- package/src/lib/config/fragments/module-boundaries.d.ts +2 -0
- package/src/lib/config/fragments/module-boundaries.js +92 -0
- package/src/lib/config/fragments/module-boundaries.js.map +1 -0
- package/src/lib/config/fragments/react-rules.d.ts +5 -0
- package/src/lib/config/fragments/react-rules.js +137 -0
- package/src/lib/config/fragments/react-rules.js.map +1 -0
- package/src/lib/config/fragments/restricted-imports.d.ts +2 -0
- package/src/lib/config/fragments/restricted-imports.js +58 -0
- package/src/lib/config/fragments/restricted-imports.js.map +1 -0
- package/src/lib/config/fragments/testing-library.d.ts +2 -0
- package/src/lib/config/fragments/testing-library.js +7 -0
- package/src/lib/config/fragments/testing-library.js.map +1 -0
- package/src/lib/config/fragments/typescript-rules.d.ts +2 -0
- package/src/lib/config/fragments/typescript-rules.js +97 -0
- package/src/lib/config/fragments/typescript-rules.js.map +1 -0
- package/src/lib/config/index.d.ts +863 -0
- package/src/lib/config/index.js +10 -0
- package/src/lib/config/index.js.map +1 -0
- package/src/lib/config/plugins.d.ts +90 -0
- package/src/lib/config/plugins.js +44 -0
- package/src/lib/config/plugins.js.map +1 -0
- package/src/lib/config/presets/base.d.ts +265 -0
- package/src/lib/config/presets/base.js +145 -0
- package/src/lib/config/presets/base.js.map +1 -0
- package/src/lib/config/presets/e2e.d.ts +10 -0
- package/src/lib/config/presets/e2e.js +19 -0
- package/src/lib/config/presets/e2e.js.map +1 -0
- package/src/lib/config/presets/public-api.d.ts +147 -0
- package/src/lib/config/presets/public-api.js +62 -0
- package/src/lib/config/presets/public-api.js.map +1 -0
- package/src/lib/config/presets/react.d.ts +598 -0
- package/src/lib/config/presets/react.js +97 -0
- package/src/lib/config/presets/react.js.map +1 -0
- package/src/lib/config/presets/server.d.ts +36 -0
- package/src/lib/config/presets/server.js +37 -0
- package/src/lib/config/presets/server.js.map +1 -0
- package/src/lib/config/utils.d.ts +6 -0
- package/src/lib/config/utils.js +28 -0
- package/src/lib/config/utils.js.map +1 -0
- package/src/lib/config-helpers/create-skip-when.d.ts +35 -0
- package/src/lib/config-helpers/create-skip-when.js +54 -0
- package/src/lib/config-helpers/create-skip-when.js.map +1 -0
- package/src/lib/rules/cva-merge-base-classes-as-array/cva-merge-base-classes-as-array.d.ts +16 -0
- package/src/lib/rules/cva-merge-base-classes-as-array/cva-merge-base-classes-as-array.js +83 -0
- package/src/lib/rules/cva-merge-base-classes-as-array/cva-merge-base-classes-as-array.js.map +1 -0
- package/src/lib/rules/design-guideline-button-icon-size-match/design-guideline-button-icon-size-match.d.ts +4 -0
- package/src/lib/rules/design-guideline-button-icon-size-match/design-guideline-button-icon-size-match.js +297 -0
- package/src/lib/rules/design-guideline-button-icon-size-match/design-guideline-button-icon-size-match.js.map +1 -0
- package/src/lib/rules/no-internal-barrel-files/examples.d.ts +80 -0
- package/src/lib/rules/no-internal-barrel-files/examples.js +84 -0
- package/src/lib/rules/no-internal-barrel-files/examples.js.map +1 -0
- package/src/lib/rules/no-internal-barrel-files/no-internal-barrel-files.d.ts +29 -0
- package/src/lib/rules/no-internal-barrel-files/no-internal-barrel-files.js +178 -0
- package/src/lib/rules/no-internal-barrel-files/no-internal-barrel-files.js.map +1 -0
- package/src/lib/rules/no-internal-graphql-when-tagged-with-gql-public/no-internal-graphql-when-tagged-with-gql-public.d.ts +5 -0
- package/src/lib/rules/no-internal-graphql-when-tagged-with-gql-public/no-internal-graphql-when-tagged-with-gql-public.js +67 -0
- package/src/lib/rules/no-internal-graphql-when-tagged-with-gql-public/no-internal-graphql-when-tagged-with-gql-public.js.map +1 -0
- package/src/lib/rules/no-jest-mock-trackunit-react-core-hooks/no-jest-mock-trackunit-react-core-hooks.d.ts +2 -0
- package/src/lib/rules/no-jest-mock-trackunit-react-core-hooks/no-jest-mock-trackunit-react-core-hooks.js +34 -0
- package/src/lib/rules/no-jest-mock-trackunit-react-core-hooks/no-jest-mock-trackunit-react-core-hooks.js.map +1 -0
- package/src/lib/rules/no-template-strings-in-classname-prop/no-template-strings-in-classname-prop.d.ts +16 -0
- package/src/lib/rules/no-template-strings-in-classname-prop/no-template-strings-in-classname-prop.js +55 -0
- package/src/lib/rules/no-template-strings-in-classname-prop/no-template-strings-in-classname-prop.js.map +1 -0
- package/src/lib/rules/no-typescript-assertion/examples.d.ts +1 -0
- package/src/lib/rules/no-typescript-assertion/examples.js +45 -0
- package/src/lib/rules/no-typescript-assertion/examples.js.map +1 -0
- package/src/lib/rules/no-typescript-assertion/no-typescript-assertion.d.ts +20 -0
- package/src/lib/rules/no-typescript-assertion/no-typescript-assertion.js +83 -0
- package/src/lib/rules/no-typescript-assertion/no-typescript-assertion.js.map +1 -0
- package/src/lib/rules/prefer-destructured-imports/prefer-destructured-imports.d.ts +73 -0
- package/src/lib/rules/prefer-destructured-imports/prefer-destructured-imports.js +333 -0
- package/src/lib/rules/prefer-destructured-imports/prefer-destructured-imports.js.map +1 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/name-suggestion-strategies.d.ts +56 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/name-suggestion-strategies.js +225 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/name-suggestion-strategies.js.map +1 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/prefer-event-specific-callback-naming.d.ts +49 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/prefer-event-specific-callback-naming.js +75 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/prefer-event-specific-callback-naming.js.map +1 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/strategies/string-based.d.ts +32 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/strategies/string-based.js +143 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/strategies/string-based.js.map +1 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/strategies/type-based.d.ts +27 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/strategies/type-based.js +196 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/strategies/type-based.js.map +1 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/utils.d.ts +76 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/utils.js +245 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/utils.js.map +1 -0
- package/src/lib/rules/prefer-field-components/prefer-field-components.d.ts +4 -0
- package/src/lib/rules/prefer-field-components/prefer-field-components.js +289 -0
- package/src/lib/rules/prefer-field-components/prefer-field-components.js.map +1 -0
- package/src/lib/rules/prefer-mouse-event-handler-in-react-props/prefer-mouse-event-handler-in-react-props.d.ts +26 -0
- package/src/lib/rules/prefer-mouse-event-handler-in-react-props/prefer-mouse-event-handler-in-react-props.js +402 -0
- package/src/lib/rules/prefer-mouse-event-handler-in-react-props/prefer-mouse-event-handler-in-react-props.js.map +1 -0
- package/src/lib/rules/require-classname-alternatives/require-classname-alternatives.d.ts +13 -0
- package/src/lib/rules/require-classname-alternatives/require-classname-alternatives.js +271 -0
- package/src/lib/rules/require-classname-alternatives/require-classname-alternatives.js.map +1 -0
- package/src/lib/rules/require-list-item-virtualization-props/require-list-item-virtualization-props.d.ts +15 -0
- package/src/lib/rules/require-list-item-virtualization-props/require-list-item-virtualization-props.js +245 -0
- package/src/lib/rules/require-list-item-virtualization-props/require-list-item-virtualization-props.js.map +1 -0
- package/src/lib/rules/require-optional-prop-initialization/require-optional-prop-initialization.d.ts +17 -0
- package/src/lib/rules/require-optional-prop-initialization/require-optional-prop-initialization.js +133 -0
- package/src/lib/rules/require-optional-prop-initialization/require-optional-prop-initialization.js.map +1 -0
- package/src/lib/rules/require-optional-prop-initialization/suggestion-utils.d.ts +12 -0
- package/src/lib/rules/require-optional-prop-initialization/suggestion-utils.js +128 -0
- package/src/lib/rules/require-optional-prop-initialization/suggestion-utils.js.map +1 -0
- package/src/lib/rules-map.d.ts +66 -0
- package/src/lib/rules-map.js +34 -0
- package/src/lib/rules-map.js.map +1 -0
- package/src/lib/utils/ast-utils.d.ts +85 -0
- package/src/lib/utils/ast-utils.js +530 -0
- package/src/lib/utils/ast-utils.js.map +1 -0
- package/src/lib/utils/classname-utils.d.ts +150 -0
- package/src/lib/utils/classname-utils.js +492 -0
- package/src/lib/utils/classname-utils.js.map +1 -0
- package/src/lib/utils/file-utils.d.ts +14 -0
- package/src/lib/utils/file-utils.js +106 -0
- package/src/lib/utils/file-utils.js.map +1 -0
- package/src/lib/utils/import-utils.d.ts +85 -0
- package/src/lib/utils/import-utils.js +193 -0
- package/src/lib/utils/import-utils.js.map +1 -0
- package/src/lib/utils/nx-utils.d.ts +59 -0
- package/src/lib/utils/nx-utils.js +103 -0
- package/src/lib/utils/nx-utils.js.map +1 -0
- package/src/lib/utils/package-utils.d.ts +38 -0
- package/src/lib/utils/package-utils.js +74 -0
- package/src/lib/utils/package-utils.js.map +1 -0
- package/src/lib/utils/typescript-utils.d.ts +29 -0
- package/src/lib/utils/typescript-utils.js +213 -0
- package/src/lib/utils/typescript-utils.js.map +1 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.preferFieldComponents = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const import_utils_1 = require("../../utils/import-utils");
|
|
6
|
+
const createRule = utils_1.ESLintUtils.RuleCreator(name => `https://github.com/trackunit/manager/blob/main/libs/eslint/plugin-trackunit/src/lib/rules/${name}/README.md`);
|
|
7
|
+
const FORM_COMPONENTS_PACKAGE = "@trackunit/react-form-components";
|
|
8
|
+
const REACT_COMPONENTS_PACKAGE = "@trackunit/react-components";
|
|
9
|
+
/**
|
|
10
|
+
* Map of base component names to their recommended Field component replacements
|
|
11
|
+
* used in the error message (human-readable, may contain alternatives).
|
|
12
|
+
*/
|
|
13
|
+
const BASE_TO_FIELD_MESSAGE_MAP = {
|
|
14
|
+
BaseSelect: "SelectField (or MultiSelectField for multi-select)",
|
|
15
|
+
CreatableSelect: "CreatableSelectField",
|
|
16
|
+
BaseInput: "TextField (or the appropriate *Field component for your input type)",
|
|
17
|
+
TextBaseInput: "TextField",
|
|
18
|
+
NumberBaseInput: "NumberField",
|
|
19
|
+
DateBaseInput: "DateField",
|
|
20
|
+
PhoneBaseInput: "PhoneField",
|
|
21
|
+
TextAreaBaseInput: "TextAreaField",
|
|
22
|
+
PasswordBaseInput: "PasswordField",
|
|
23
|
+
EmailBaseInput: "EmailField",
|
|
24
|
+
UrlBaseInput: "UrlField",
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Map used for autofix — always resolves to a single concrete component name.
|
|
28
|
+
*
|
|
29
|
+
* NOTE: BaseSelect and CreatableSelect are intentionally excluded here.
|
|
30
|
+
* SelectField and CreatableSelectField have different value/onChange APIs
|
|
31
|
+
* compared to their base counterparts:
|
|
32
|
+
* - BaseSelect uses `value={optionObject}` and `onChange={(option) => ...}`
|
|
33
|
+
* - SelectField uses `value={rawValue}` and `onChange={(event) => ...}`
|
|
34
|
+
* Auto-fixing would silently break the component's behaviour.
|
|
35
|
+
*
|
|
36
|
+
* BaseSelect with isMulti → MultiSelectField IS compatible and is handled
|
|
37
|
+
* separately in getAutofixFieldName().
|
|
38
|
+
*/
|
|
39
|
+
const BASE_TO_FIELD_AUTOFIX_MAP = {
|
|
40
|
+
BaseInput: "TextField",
|
|
41
|
+
TextBaseInput: "TextField",
|
|
42
|
+
NumberBaseInput: "NumberField",
|
|
43
|
+
DateBaseInput: "DateField",
|
|
44
|
+
PhoneBaseInput: "PhoneField",
|
|
45
|
+
TextAreaBaseInput: "TextAreaField",
|
|
46
|
+
PasswordBaseInput: "PasswordField",
|
|
47
|
+
EmailBaseInput: "EmailField",
|
|
48
|
+
UrlBaseInput: "UrlField",
|
|
49
|
+
};
|
|
50
|
+
const BASE_COMPONENT_NAMES = new Set(Object.keys(BASE_TO_FIELD_MESSAGE_MAP));
|
|
51
|
+
/**
|
|
52
|
+
* Component names that act as a manual label above a base input/select.
|
|
53
|
+
*/
|
|
54
|
+
const LABEL_COMPONENT_NAMES = new Set(["Label", "Text"]);
|
|
55
|
+
/**
|
|
56
|
+
* Get the JSX element name from a JSXOpeningElement.
|
|
57
|
+
* Returns null for member expressions or namespaced names.
|
|
58
|
+
*/
|
|
59
|
+
function getJSXElementName(openingElement) {
|
|
60
|
+
if (openingElement.name.type === utils_1.AST_NODE_TYPES.JSXIdentifier) {
|
|
61
|
+
return openingElement.name.name;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Check if a JSX child is a JSXText node that only contains whitespace.
|
|
67
|
+
*/
|
|
68
|
+
function isWhitespaceText(child) {
|
|
69
|
+
return child.type === utils_1.AST_NODE_TYPES.JSXText && child.value.trim() === "";
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Check if a JSX child is a label-like element (Label or Text).
|
|
73
|
+
*/
|
|
74
|
+
function isLabelLikeElement(child) {
|
|
75
|
+
if (child.type !== utils_1.AST_NODE_TYPES.JSXElement) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
const name = getJSXElementName(child.openingElement);
|
|
79
|
+
return name !== null && LABEL_COMPONENT_NAMES.has(name);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Find the previous non-whitespace sibling in a list of JSX children.
|
|
83
|
+
*/
|
|
84
|
+
function getPreviousNonWhitespaceSibling(children, currentIndex) {
|
|
85
|
+
for (let i = currentIndex - 1; i >= 0; i--) {
|
|
86
|
+
const sibling = children[i];
|
|
87
|
+
if (sibling !== undefined && !isWhitespaceText(sibling)) {
|
|
88
|
+
return sibling;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Check if a JSXOpeningElement has a specific boolean or truthy prop.
|
|
95
|
+
*/
|
|
96
|
+
function hasJSXProp(openingElement, propName) {
|
|
97
|
+
return openingElement.attributes.some(attr => attr.type === utils_1.AST_NODE_TYPES.JSXAttribute &&
|
|
98
|
+
attr.name.type === utils_1.AST_NODE_TYPES.JSXIdentifier &&
|
|
99
|
+
attr.name.name === propName);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Extract the meaningful label content from a Label/Text element's children.
|
|
103
|
+
*
|
|
104
|
+
* Returns null if the content is too complex to autofix (e.g. multiple meaningful
|
|
105
|
+
* children, or JSX element children).
|
|
106
|
+
*/
|
|
107
|
+
function extractLabelContent(labelElement, sourceCode) {
|
|
108
|
+
// Filter out whitespace-only text children
|
|
109
|
+
const meaningfulChildren = labelElement.children.filter(node => {
|
|
110
|
+
if (node.type === utils_1.AST_NODE_TYPES.JSXText) {
|
|
111
|
+
return node.value.trim() !== "";
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
});
|
|
115
|
+
if (meaningfulChildren.length !== 1) {
|
|
116
|
+
return null; // Complex content — skip autofix
|
|
117
|
+
}
|
|
118
|
+
const child = meaningfulChildren[0];
|
|
119
|
+
if (child === undefined) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
if (child.type === utils_1.AST_NODE_TYPES.JSXText) {
|
|
123
|
+
const trimmed = child.value.trim();
|
|
124
|
+
if (trimmed.length === 0) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
return { type: "string", value: trimmed };
|
|
128
|
+
}
|
|
129
|
+
if (child.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer) {
|
|
130
|
+
if (child.expression.type === utils_1.AST_NODE_TYPES.JSXEmptyExpression) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
return { type: "expression", text: sourceCode.getText(child.expression) };
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Determine the field component name for autofix.
|
|
139
|
+
*
|
|
140
|
+
* Returns null (no autofix) for BaseSelect without isMulti and CreatableSelect,
|
|
141
|
+
* because SelectField/CreatableSelectField have incompatible value/onChange APIs.
|
|
142
|
+
*
|
|
143
|
+
* BaseSelect with isMulti → MultiSelectField is safe because MultiSelectField
|
|
144
|
+
* preserves the same option-object-based value/onChange API as BaseSelect.
|
|
145
|
+
*/
|
|
146
|
+
function getAutofixFieldName(baseComponentName, openingElement) {
|
|
147
|
+
if (baseComponentName === "BaseSelect" && hasJSXProp(openingElement, "isMulti")) {
|
|
148
|
+
return "MultiSelectField";
|
|
149
|
+
}
|
|
150
|
+
return BASE_TO_FIELD_AUTOFIX_MAP[baseComponentName] ?? null;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Build the label prop string for the autofix.
|
|
154
|
+
*/
|
|
155
|
+
function buildLabelPropValue(content) {
|
|
156
|
+
switch (content.type) {
|
|
157
|
+
case "string":
|
|
158
|
+
return `"${content.value}"`;
|
|
159
|
+
case "expression":
|
|
160
|
+
return `{${content.text}}`;
|
|
161
|
+
default: {
|
|
162
|
+
const _exhaustiveCheck = content;
|
|
163
|
+
return _exhaustiveCheck;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Count how many times a component is used as a JSX opening tag in the source text.
|
|
169
|
+
* Uses a regex that matches `<ComponentName` followed by whitespace, `>`, or `/>`.
|
|
170
|
+
*/
|
|
171
|
+
function countJSXElementUsages(sourceText, componentName) {
|
|
172
|
+
const pattern = new RegExp(`<${componentName}(?=[\\s/>])`, "g");
|
|
173
|
+
const matches = sourceText.match(pattern);
|
|
174
|
+
return matches ? matches.length : 0;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Build a new import statement string with the given specifiers, preserving semicolons.
|
|
178
|
+
*/
|
|
179
|
+
function buildImportStatement(sourceCode, importNode, specifiers) {
|
|
180
|
+
const sortedSpecifiers = Array.from(specifiers).sort().join(", ");
|
|
181
|
+
const originalText = sourceCode.getText(importNode);
|
|
182
|
+
const semicolon = originalText.trimEnd().endsWith(";") ? ";" : "";
|
|
183
|
+
return `import { ${sortedSpecifiers} } from "${importNode.source.value}"${semicolon}`;
|
|
184
|
+
}
|
|
185
|
+
exports.preferFieldComponents = createRule({
|
|
186
|
+
name: "prefer-field-components",
|
|
187
|
+
meta: {
|
|
188
|
+
type: "suggestion",
|
|
189
|
+
fixable: "code",
|
|
190
|
+
docs: {
|
|
191
|
+
description: "Prefer using Field components (e.g. SelectField, TextField) instead of manually combining Label/Text with base input components (e.g. BaseSelect, BaseInput).",
|
|
192
|
+
},
|
|
193
|
+
schema: [],
|
|
194
|
+
messages: {
|
|
195
|
+
preferFieldComponent: "Avoid using {{labelComponent}} with {{baseComponent}}. Use {{fieldComponent}} instead, which includes a built-in label via the `label` prop.",
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
defaultOptions: [],
|
|
199
|
+
create(context) {
|
|
200
|
+
const sourceCode = context.sourceCode;
|
|
201
|
+
return {
|
|
202
|
+
JSXElement(node) {
|
|
203
|
+
const children = node.children;
|
|
204
|
+
for (let i = 0; i < children.length; i++) {
|
|
205
|
+
const child = children[i];
|
|
206
|
+
// Only look at JSXElement children
|
|
207
|
+
if (child === undefined || child.type !== utils_1.AST_NODE_TYPES.JSXElement) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const childName = getJSXElementName(child.openingElement);
|
|
211
|
+
// Check if this child is a base component
|
|
212
|
+
if (childName === null || !BASE_COMPONENT_NAMES.has(childName)) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
// Found a base component — check if the previous non-whitespace sibling is a Label/Text
|
|
216
|
+
const previousSibling = getPreviousNonWhitespaceSibling(children, i);
|
|
217
|
+
if (previousSibling === null || !isLabelLikeElement(previousSibling)) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const labelName = getJSXElementName(previousSibling.openingElement);
|
|
221
|
+
const fieldComponentMessage = BASE_TO_FIELD_MESSAGE_MAP[childName] ?? "the appropriate *Field component";
|
|
222
|
+
const fieldComponentAutofix = getAutofixFieldName(childName, child.openingElement);
|
|
223
|
+
const labelContent = extractLabelContent(previousSibling, sourceCode);
|
|
224
|
+
context.report({
|
|
225
|
+
node: child,
|
|
226
|
+
messageId: "preferFieldComponent",
|
|
227
|
+
data: {
|
|
228
|
+
labelComponent: labelName ?? "Label/Text",
|
|
229
|
+
baseComponent: childName,
|
|
230
|
+
fieldComponent: fieldComponentMessage,
|
|
231
|
+
},
|
|
232
|
+
fix: fieldComponentAutofix !== null && labelContent !== null
|
|
233
|
+
? fixer => {
|
|
234
|
+
const fixes = [];
|
|
235
|
+
const labelPropValue = buildLabelPropValue(labelContent);
|
|
236
|
+
const fullSourceText = sourceCode.getText();
|
|
237
|
+
// 1. Replace everything from Label start through the base component's tag name
|
|
238
|
+
// in a single operation: <Label>...</Label>\n...<BaseSelect → <SelectField label={...}
|
|
239
|
+
fixes.push(fixer.replaceTextRange([previousSibling.range[0], child.openingElement.name.range[1]], `<${fieldComponentAutofix} label=${labelPropValue}`));
|
|
240
|
+
// 2. Rename the closing tag if present (for non-self-closing elements)
|
|
241
|
+
if (child.closingElement !== null) {
|
|
242
|
+
fixes.push(fixer.replaceText(child.closingElement.name, fieldComponentAutofix));
|
|
243
|
+
}
|
|
244
|
+
// 3. Update imports: add the field component, remove base/label if no longer used.
|
|
245
|
+
// All modifications to the same import declaration are combined into a single fix.
|
|
246
|
+
const baseUsageCount = countJSXElementUsages(fullSourceText, childName);
|
|
247
|
+
const labelUsageCount = labelName !== null ? countJSXElementUsages(fullSourceText, labelName) : 0;
|
|
248
|
+
// Handle @trackunit/react-form-components import
|
|
249
|
+
const formImport = (0, import_utils_1.findImportDeclaration)(sourceCode, FORM_COMPONENTS_PACKAGE);
|
|
250
|
+
if (formImport) {
|
|
251
|
+
const specifiers = (0, import_utils_1.getImportSpecifiers)(formImport);
|
|
252
|
+
// Add field component
|
|
253
|
+
specifiers.add(fieldComponentAutofix);
|
|
254
|
+
// Remove base component if this is the only JSX usage
|
|
255
|
+
if (baseUsageCount <= 1) {
|
|
256
|
+
specifiers.delete(childName);
|
|
257
|
+
}
|
|
258
|
+
// Remove Label if it's from this package and this is the only JSX usage
|
|
259
|
+
if (labelName === "Label" && labelUsageCount <= 1) {
|
|
260
|
+
specifiers.delete("Label");
|
|
261
|
+
}
|
|
262
|
+
fixes.push(fixer.replaceText(formImport, buildImportStatement(sourceCode, formImport, specifiers)));
|
|
263
|
+
}
|
|
264
|
+
// Handle @trackunit/react-components import (for Text removal)
|
|
265
|
+
if (labelName === "Text" && labelUsageCount <= 1) {
|
|
266
|
+
const reactImport = (0, import_utils_1.findImportDeclaration)(sourceCode, REACT_COMPONENTS_PACKAGE);
|
|
267
|
+
if (reactImport) {
|
|
268
|
+
const specifiers = (0, import_utils_1.getImportSpecifiers)(reactImport);
|
|
269
|
+
if (specifiers.has("Text")) {
|
|
270
|
+
specifiers.delete("Text");
|
|
271
|
+
if (specifiers.size === 0) {
|
|
272
|
+
fixes.push(fixer.remove(reactImport));
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
fixes.push(fixer.replaceText(reactImport, buildImportStatement(sourceCode, reactImport, specifiers)));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return fixes;
|
|
281
|
+
}
|
|
282
|
+
: null,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
//# sourceMappingURL=prefer-field-components.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prefer-field-components.js","sourceRoot":"","sources":["../../../../../../../../libs/eslint/plugin-trackunit/src/lib/rules/prefer-field-components/prefer-field-components.ts"],"names":[],"mappings":"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","sourcesContent":["import { AST_NODE_TYPES, ESLintUtils, TSESTree } from \"@typescript-eslint/utils\";\nimport { findImportDeclaration, getImportSpecifiers } from \"../../utils/import-utils\";\n\nconst createRule = ESLintUtils.RuleCreator(\n name => `https://github.com/trackunit/manager/blob/main/libs/eslint/plugin-trackunit/src/lib/rules/${name}/README.md`\n);\n\ntype MessageIds = \"preferFieldComponent\";\n\nconst FORM_COMPONENTS_PACKAGE = \"@trackunit/react-form-components\";\nconst REACT_COMPONENTS_PACKAGE = \"@trackunit/react-components\";\n\n/**\n * Map of base component names to their recommended Field component replacements\n * used in the error message (human-readable, may contain alternatives).\n */\nconst BASE_TO_FIELD_MESSAGE_MAP: Record<string, string> = {\n BaseSelect: \"SelectField (or MultiSelectField for multi-select)\",\n CreatableSelect: \"CreatableSelectField\",\n BaseInput: \"TextField (or the appropriate *Field component for your input type)\",\n TextBaseInput: \"TextField\",\n NumberBaseInput: \"NumberField\",\n DateBaseInput: \"DateField\",\n PhoneBaseInput: \"PhoneField\",\n TextAreaBaseInput: \"TextAreaField\",\n PasswordBaseInput: \"PasswordField\",\n EmailBaseInput: \"EmailField\",\n UrlBaseInput: \"UrlField\",\n};\n\n/**\n * Map used for autofix — always resolves to a single concrete component name.\n *\n * NOTE: BaseSelect and CreatableSelect are intentionally excluded here.\n * SelectField and CreatableSelectField have different value/onChange APIs\n * compared to their base counterparts:\n * - BaseSelect uses `value={optionObject}` and `onChange={(option) => ...}`\n * - SelectField uses `value={rawValue}` and `onChange={(event) => ...}`\n * Auto-fixing would silently break the component's behaviour.\n *\n * BaseSelect with isMulti → MultiSelectField IS compatible and is handled\n * separately in getAutofixFieldName().\n */\nconst BASE_TO_FIELD_AUTOFIX_MAP: Record<string, string> = {\n BaseInput: \"TextField\",\n TextBaseInput: \"TextField\",\n NumberBaseInput: \"NumberField\",\n DateBaseInput: \"DateField\",\n PhoneBaseInput: \"PhoneField\",\n TextAreaBaseInput: \"TextAreaField\",\n PasswordBaseInput: \"PasswordField\",\n EmailBaseInput: \"EmailField\",\n UrlBaseInput: \"UrlField\",\n};\n\nconst BASE_COMPONENT_NAMES = new Set(Object.keys(BASE_TO_FIELD_MESSAGE_MAP));\n\n/**\n * Component names that act as a manual label above a base input/select.\n */\nconst LABEL_COMPONENT_NAMES = new Set([\"Label\", \"Text\"]);\n\n/**\n * Get the JSX element name from a JSXOpeningElement.\n * Returns null for member expressions or namespaced names.\n */\nfunction getJSXElementName(openingElement: TSESTree.JSXOpeningElement): string | null {\n if (openingElement.name.type === AST_NODE_TYPES.JSXIdentifier) {\n return openingElement.name.name;\n }\n return null;\n}\n\n/**\n * Check if a JSX child is a JSXText node that only contains whitespace.\n */\nfunction isWhitespaceText(child: TSESTree.JSXChild): boolean {\n return child.type === AST_NODE_TYPES.JSXText && child.value.trim() === \"\";\n}\n\n/**\n * Check if a JSX child is a label-like element (Label or Text).\n */\nfunction isLabelLikeElement(child: TSESTree.JSXChild): child is TSESTree.JSXElement {\n if (child.type !== AST_NODE_TYPES.JSXElement) {\n return false;\n }\n const name = getJSXElementName(child.openingElement);\n return name !== null && LABEL_COMPONENT_NAMES.has(name);\n}\n\n/**\n * Find the previous non-whitespace sibling in a list of JSX children.\n */\nfunction getPreviousNonWhitespaceSibling(\n children: Array<TSESTree.JSXChild>,\n currentIndex: number\n): TSESTree.JSXChild | null {\n for (let i = currentIndex - 1; i >= 0; i--) {\n const sibling = children[i];\n if (sibling !== undefined && !isWhitespaceText(sibling)) {\n return sibling;\n }\n }\n return null;\n}\n\n/**\n * Check if a JSXOpeningElement has a specific boolean or truthy prop.\n */\nfunction hasJSXProp(openingElement: TSESTree.JSXOpeningElement, propName: string): boolean {\n return openingElement.attributes.some(\n attr =>\n attr.type === AST_NODE_TYPES.JSXAttribute &&\n attr.name.type === AST_NODE_TYPES.JSXIdentifier &&\n attr.name.name === propName\n );\n}\n\ntype LabelContent = { type: \"string\"; value: string } | { type: \"expression\"; text: string };\n\n/**\n * Extract the meaningful label content from a Label/Text element's children.\n *\n * Returns null if the content is too complex to autofix (e.g. multiple meaningful\n * children, or JSX element children).\n */\nfunction extractLabelContent(\n labelElement: TSESTree.JSXElement,\n sourceCode: Readonly<{ getText(node: TSESTree.Node): string }>\n): LabelContent | null {\n // Filter out whitespace-only text children\n const meaningfulChildren = labelElement.children.filter(node => {\n if (node.type === AST_NODE_TYPES.JSXText) {\n return node.value.trim() !== \"\";\n }\n return true;\n });\n\n if (meaningfulChildren.length !== 1) {\n return null; // Complex content — skip autofix\n }\n\n const child = meaningfulChildren[0];\n if (child === undefined) {\n return null;\n }\n\n if (child.type === AST_NODE_TYPES.JSXText) {\n const trimmed = child.value.trim();\n if (trimmed.length === 0) {\n return null;\n }\n return { type: \"string\", value: trimmed };\n }\n\n if (child.type === AST_NODE_TYPES.JSXExpressionContainer) {\n if (child.expression.type === AST_NODE_TYPES.JSXEmptyExpression) {\n return null;\n }\n return { type: \"expression\", text: sourceCode.getText(child.expression) };\n }\n\n return null;\n}\n\n/**\n * Determine the field component name for autofix.\n *\n * Returns null (no autofix) for BaseSelect without isMulti and CreatableSelect,\n * because SelectField/CreatableSelectField have incompatible value/onChange APIs.\n *\n * BaseSelect with isMulti → MultiSelectField is safe because MultiSelectField\n * preserves the same option-object-based value/onChange API as BaseSelect.\n */\nfunction getAutofixFieldName(baseComponentName: string, openingElement: TSESTree.JSXOpeningElement): string | null {\n if (baseComponentName === \"BaseSelect\" && hasJSXProp(openingElement, \"isMulti\")) {\n return \"MultiSelectField\";\n }\n return BASE_TO_FIELD_AUTOFIX_MAP[baseComponentName] ?? null;\n}\n\n/**\n * Build the label prop string for the autofix.\n */\nfunction buildLabelPropValue(content: LabelContent): string {\n switch (content.type) {\n case \"string\":\n return `\"${content.value}\"`;\n case \"expression\":\n return `{${content.text}}`;\n default: {\n const _exhaustiveCheck: never = content;\n return _exhaustiveCheck;\n }\n }\n}\n\n/**\n * Count how many times a component is used as a JSX opening tag in the source text.\n * Uses a regex that matches `<ComponentName` followed by whitespace, `>`, or `/>`.\n */\nfunction countJSXElementUsages(sourceText: string, componentName: string): number {\n const pattern = new RegExp(`<${componentName}(?=[\\\\s/>])`, \"g\");\n const matches = sourceText.match(pattern);\n return matches ? matches.length : 0;\n}\n\n/**\n * Build a new import statement string with the given specifiers, preserving semicolons.\n */\nfunction buildImportStatement(\n sourceCode: Readonly<{ getText(node: TSESTree.Node): string }>,\n importNode: TSESTree.ImportDeclaration,\n specifiers: Iterable<string>\n): string {\n const sortedSpecifiers = Array.from(specifiers).sort().join(\", \");\n const originalText = sourceCode.getText(importNode);\n const semicolon = originalText.trimEnd().endsWith(\";\") ? \";\" : \"\";\n return `import { ${sortedSpecifiers} } from \"${importNode.source.value}\"${semicolon}`;\n}\n\nexport const preferFieldComponents = createRule<[], MessageIds>({\n name: \"prefer-field-components\",\n meta: {\n type: \"suggestion\",\n fixable: \"code\",\n docs: {\n description:\n \"Prefer using Field components (e.g. SelectField, TextField) instead of manually combining Label/Text with base input components (e.g. BaseSelect, BaseInput).\",\n },\n schema: [],\n messages: {\n preferFieldComponent:\n \"Avoid using {{labelComponent}} with {{baseComponent}}. Use {{fieldComponent}} instead, which includes a built-in label via the `label` prop.\",\n },\n },\n defaultOptions: [],\n create(context) {\n const sourceCode = context.sourceCode;\n\n return {\n JSXElement(node: TSESTree.JSXElement) {\n const children = node.children;\n\n for (let i = 0; i < children.length; i++) {\n const child = children[i];\n\n // Only look at JSXElement children\n if (child === undefined || child.type !== AST_NODE_TYPES.JSXElement) {\n continue;\n }\n\n const childName = getJSXElementName(child.openingElement);\n\n // Check if this child is a base component\n if (childName === null || !BASE_COMPONENT_NAMES.has(childName)) {\n continue;\n }\n\n // Found a base component — check if the previous non-whitespace sibling is a Label/Text\n const previousSibling = getPreviousNonWhitespaceSibling(children, i);\n\n if (previousSibling === null || !isLabelLikeElement(previousSibling)) {\n continue;\n }\n\n const labelName = getJSXElementName(previousSibling.openingElement);\n const fieldComponentMessage = BASE_TO_FIELD_MESSAGE_MAP[childName] ?? \"the appropriate *Field component\";\n const fieldComponentAutofix = getAutofixFieldName(childName, child.openingElement);\n const labelContent = extractLabelContent(previousSibling, sourceCode);\n\n context.report({\n node: child,\n messageId: \"preferFieldComponent\",\n data: {\n labelComponent: labelName ?? \"Label/Text\",\n baseComponent: childName,\n fieldComponent: fieldComponentMessage,\n },\n fix:\n fieldComponentAutofix !== null && labelContent !== null\n ? fixer => {\n const fixes: Array<ReturnType<typeof fixer.replaceText>> = [];\n const labelPropValue = buildLabelPropValue(labelContent);\n const fullSourceText = sourceCode.getText();\n\n // 1. Replace everything from Label start through the base component's tag name\n // in a single operation: <Label>...</Label>\\n...<BaseSelect → <SelectField label={...}\n fixes.push(\n fixer.replaceTextRange(\n [previousSibling.range[0], child.openingElement.name.range[1]],\n `<${fieldComponentAutofix} label=${labelPropValue}`\n )\n );\n\n // 2. Rename the closing tag if present (for non-self-closing elements)\n if (child.closingElement !== null) {\n fixes.push(fixer.replaceText(child.closingElement.name, fieldComponentAutofix));\n }\n\n // 3. Update imports: add the field component, remove base/label if no longer used.\n // All modifications to the same import declaration are combined into a single fix.\n const baseUsageCount = countJSXElementUsages(fullSourceText, childName);\n const labelUsageCount = labelName !== null ? countJSXElementUsages(fullSourceText, labelName) : 0;\n\n // Handle @trackunit/react-form-components import\n const formImport = findImportDeclaration(sourceCode, FORM_COMPONENTS_PACKAGE);\n if (formImport) {\n const specifiers = getImportSpecifiers(formImport);\n\n // Add field component\n specifiers.add(fieldComponentAutofix);\n\n // Remove base component if this is the only JSX usage\n if (baseUsageCount <= 1) {\n specifiers.delete(childName);\n }\n\n // Remove Label if it's from this package and this is the only JSX usage\n if (labelName === \"Label\" && labelUsageCount <= 1) {\n specifiers.delete(\"Label\");\n }\n\n fixes.push(\n fixer.replaceText(formImport, buildImportStatement(sourceCode, formImport, specifiers))\n );\n }\n\n // Handle @trackunit/react-components import (for Text removal)\n if (labelName === \"Text\" && labelUsageCount <= 1) {\n const reactImport = findImportDeclaration(sourceCode, REACT_COMPONENTS_PACKAGE);\n if (reactImport) {\n const specifiers = getImportSpecifiers(reactImport);\n if (specifiers.has(\"Text\")) {\n specifiers.delete(\"Text\");\n if (specifiers.size === 0) {\n fixes.push(fixer.remove(reactImport));\n } else {\n fixes.push(\n fixer.replaceText(reactImport, buildImportStatement(sourceCode, reactImport, specifiers))\n );\n }\n }\n }\n }\n\n return fixes;\n }\n : null,\n });\n }\n },\n };\n },\n});\n"]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: prefer-mouse-event-handler-in-react-props
|
|
3
|
+
*
|
|
4
|
+
* Enforces using MouseEventHandler<T> instead of () => void or (e: MouseEvent) => void
|
|
5
|
+
* for onClick* props in React components. This ensures proper React typing and consistency.
|
|
6
|
+
*
|
|
7
|
+
* Only flags properties in actual React component props, not in hook options or other interfaces.
|
|
8
|
+
*
|
|
9
|
+
* Uses ESLint suggestions (not autofix) to let users choose the appropriate element type.
|
|
10
|
+
*/
|
|
11
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
12
|
+
type Options = [
|
|
13
|
+
{
|
|
14
|
+
/**
|
|
15
|
+
* Prop names to exempt from this rule.
|
|
16
|
+
*
|
|
17
|
+
* @example ["onClick"]
|
|
18
|
+
*/
|
|
19
|
+
allowedNames?: ReadonlyArray<string>;
|
|
20
|
+
}
|
|
21
|
+
];
|
|
22
|
+
type MessageIds = "preferMouseEventHandler" | "suggestMouseEventHandler";
|
|
23
|
+
export declare const preferMouseEventHandlerInReactProps: ESLintUtils.RuleModule<MessageIds, Options, unknown, ESLintUtils.RuleListener> & {
|
|
24
|
+
name: string;
|
|
25
|
+
};
|
|
26
|
+
export {};
|