@codyswann/lisa 1.50.1 → 1.50.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/node_modules/@codyswann/eslint-plugin-code-organization/README.md +149 -0
- package/node_modules/@codyswann/eslint-plugin-code-organization/__tests__/enforce-statement-order.test.js +473 -0
- package/node_modules/@codyswann/eslint-plugin-code-organization/index.js +28 -0
- package/node_modules/@codyswann/eslint-plugin-code-organization/package.json +10 -0
- package/node_modules/@codyswann/eslint-plugin-code-organization/rules/enforce-statement-order.js +162 -0
- package/node_modules/@codyswann/eslint-plugin-component-structure/README.md +234 -0
- package/node_modules/@codyswann/eslint-plugin-component-structure/__tests__/plugin-index.test.js +89 -0
- package/node_modules/@codyswann/eslint-plugin-component-structure/__tests__/require-memo-in-view.test.js +201 -0
- package/node_modules/@codyswann/eslint-plugin-component-structure/__tests__/single-component-per-file.test.js +294 -0
- package/node_modules/@codyswann/eslint-plugin-component-structure/index.js +37 -0
- package/node_modules/@codyswann/eslint-plugin-component-structure/package.json +10 -0
- package/node_modules/@codyswann/eslint-plugin-component-structure/rules/enforce-component-structure.js +235 -0
- package/node_modules/@codyswann/eslint-plugin-component-structure/rules/no-return-in-view.js +96 -0
- package/node_modules/@codyswann/eslint-plugin-component-structure/rules/require-memo-in-view.js +183 -0
- package/node_modules/@codyswann/eslint-plugin-component-structure/rules/single-component-per-file.js +243 -0
- package/node_modules/@codyswann/eslint-plugin-ui-standards/README.md +192 -0
- package/node_modules/@codyswann/eslint-plugin-ui-standards/index.js +31 -0
- package/node_modules/@codyswann/eslint-plugin-ui-standards/package.json +10 -0
- package/node_modules/@codyswann/eslint-plugin-ui-standards/rules/no-classname-outside-ui.js +56 -0
- package/node_modules/@codyswann/eslint-plugin-ui-standards/rules/no-direct-rn-imports.js +60 -0
- package/package.json +6 -1
package/node_modules/@codyswann/eslint-plugin-component-structure/rules/require-memo-in-view.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is managed by Lisa.
|
|
3
|
+
* Do not edit directly — changes will be overwritten on the next `lisa` run.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ESLint rule to enforce React.memo usage with displayName in View components
|
|
8
|
+
*
|
|
9
|
+
* This rule ensures that all View components (*View.tsx, *View.jsx) follow the standardized pattern:
|
|
10
|
+
* - Must be wrapped with memo() or React.memo() in the default export
|
|
11
|
+
* - Must have a displayName property
|
|
12
|
+
*
|
|
13
|
+
* Excludes components/ui/** and components/custom/ui/** directories (third-party generated files)
|
|
14
|
+
* @module eslint-plugin-component-structure/rules/require-memo-in-view
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
meta: {
|
|
19
|
+
type: "problem",
|
|
20
|
+
docs: {
|
|
21
|
+
description:
|
|
22
|
+
"Enforce React.memo usage with displayName in View components",
|
|
23
|
+
category: "Best Practices",
|
|
24
|
+
recommended: true,
|
|
25
|
+
},
|
|
26
|
+
schema: [],
|
|
27
|
+
messages: {
|
|
28
|
+
missingMemo:
|
|
29
|
+
"View components must be wrapped with memo(). Expected: export default memo({{componentName}})",
|
|
30
|
+
missingDisplayName:
|
|
31
|
+
'View components must have a displayName property. Expected: {{componentName}}.displayName = "{{componentName}}"',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
create(context) {
|
|
36
|
+
const filename = context.getFilename();
|
|
37
|
+
const normalizedPath = filename.replace(/\\/g, "/");
|
|
38
|
+
|
|
39
|
+
// Only check View.tsx and View.jsx files
|
|
40
|
+
if (!filename.endsWith("View.tsx") && !filename.endsWith("View.jsx")) {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Exclude components/ui/** and components/custom/ui/** directories
|
|
45
|
+
if (
|
|
46
|
+
normalizedPath.includes("/components/ui/") ||
|
|
47
|
+
normalizedPath.includes("/components/custom/ui/")
|
|
48
|
+
) {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check if file is in features/**/components, features/**/screens, or components directory
|
|
53
|
+
const isFeatureComponent =
|
|
54
|
+
normalizedPath.includes("features/") &&
|
|
55
|
+
normalizedPath.includes("/components/");
|
|
56
|
+
const isFeatureScreen =
|
|
57
|
+
normalizedPath.includes("features/") &&
|
|
58
|
+
normalizedPath.includes("/screens/");
|
|
59
|
+
const isComponentsDir = normalizedPath.includes("/components/");
|
|
60
|
+
|
|
61
|
+
if (!isFeatureComponent && !isFeatureScreen && !isComponentsDir) {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const state = {
|
|
66
|
+
componentName: null,
|
|
67
|
+
hasMemoImport: false,
|
|
68
|
+
hasMemoWrapper: false,
|
|
69
|
+
hasDisplayName: false,
|
|
70
|
+
exportNode: null,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
ImportDeclaration(node) {
|
|
75
|
+
// Check if memo is imported from 'react'
|
|
76
|
+
if (node.source.value === "react") {
|
|
77
|
+
const memoImport = node.specifiers.find(
|
|
78
|
+
spec =>
|
|
79
|
+
spec.type === "ImportSpecifier" && spec.imported.name === "memo"
|
|
80
|
+
);
|
|
81
|
+
if (memoImport) {
|
|
82
|
+
state.hasMemoImport = true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
VariableDeclarator(node) {
|
|
88
|
+
// Find the component name from variable declaration
|
|
89
|
+
if (
|
|
90
|
+
node.id.type === "Identifier" &&
|
|
91
|
+
/^[A-Z]/.test(node.id.name) &&
|
|
92
|
+
node.id.name.includes("View")
|
|
93
|
+
) {
|
|
94
|
+
state.componentName = node.id.name;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
AssignmentExpression(node) {
|
|
99
|
+
// Check for displayName assignment
|
|
100
|
+
if (
|
|
101
|
+
node.left.type === "MemberExpression" &&
|
|
102
|
+
node.left.property.name === "displayName" &&
|
|
103
|
+
node.left.object.type === "Identifier"
|
|
104
|
+
) {
|
|
105
|
+
const componentName = node.left.object.name;
|
|
106
|
+
if (
|
|
107
|
+
componentName === state.componentName ||
|
|
108
|
+
/^[A-Z]/.test(componentName)
|
|
109
|
+
) {
|
|
110
|
+
state.hasDisplayName = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
ExportDefaultDeclaration(node) {
|
|
116
|
+
state.exportNode = node;
|
|
117
|
+
|
|
118
|
+
// Check if the default export is wrapped with memo()
|
|
119
|
+
if (node.declaration.type === "CallExpression") {
|
|
120
|
+
const callee = node.declaration.callee;
|
|
121
|
+
|
|
122
|
+
// Check for memo() or React.memo()
|
|
123
|
+
const isMemoCall =
|
|
124
|
+
(callee.type === "Identifier" && callee.name === "memo") ||
|
|
125
|
+
(callee.type === "MemberExpression" &&
|
|
126
|
+
callee.object.name === "React" &&
|
|
127
|
+
callee.property.name === "memo");
|
|
128
|
+
|
|
129
|
+
if (isMemoCall) {
|
|
130
|
+
state.hasMemoWrapper = true;
|
|
131
|
+
|
|
132
|
+
// If using React.memo, ensure memo is imported from 'react'
|
|
133
|
+
if (callee.type === "MemberExpression" && !state.hasMemoImport) {
|
|
134
|
+
// React.memo is allowed, but we prefer direct memo import
|
|
135
|
+
// Store that we found React.memo usage
|
|
136
|
+
state.hasReactMemo = true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Get the component name from the memo argument
|
|
140
|
+
const firstArg = node.declaration.arguments[0];
|
|
141
|
+
if (
|
|
142
|
+
firstArg &&
|
|
143
|
+
firstArg.type === "Identifier" &&
|
|
144
|
+
!state.componentName
|
|
145
|
+
) {
|
|
146
|
+
state.componentName = firstArg.name;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} else if (
|
|
150
|
+
node.declaration.type === "Identifier" &&
|
|
151
|
+
!state.componentName
|
|
152
|
+
) {
|
|
153
|
+
// Component exported without memo wrapper
|
|
154
|
+
state.componentName = node.declaration.name;
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
"Program:exit"() {
|
|
159
|
+
if (!state.exportNode || !state.componentName) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Report missing memo wrapper
|
|
164
|
+
if (!state.hasMemoWrapper) {
|
|
165
|
+
context.report({
|
|
166
|
+
node: state.exportNode,
|
|
167
|
+
messageId: "missingMemo",
|
|
168
|
+
data: { componentName: state.componentName },
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Report missing displayName
|
|
173
|
+
if (!state.hasDisplayName) {
|
|
174
|
+
context.report({
|
|
175
|
+
node: state.exportNode,
|
|
176
|
+
messageId: "missingDisplayName",
|
|
177
|
+
data: { componentName: state.componentName },
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
};
|
package/node_modules/@codyswann/eslint-plugin-component-structure/rules/single-component-per-file.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is managed by Lisa.
|
|
3
|
+
* Do not edit directly — changes will be overwritten on the next `lisa` run.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ESLint rule to enforce exactly one React component per file
|
|
8
|
+
*
|
|
9
|
+
* This rule ensures that View and Container files contain only one React component.
|
|
10
|
+
* A React component is defined as any PascalCase function that returns JSX.
|
|
11
|
+
*
|
|
12
|
+
* Applies to:
|
|
13
|
+
* - *View.tsx, *View.jsx files
|
|
14
|
+
* - *Container.tsx, *Container.jsx files
|
|
15
|
+
*
|
|
16
|
+
* Excludes:
|
|
17
|
+
* - components/ui/** directory (third-party generated files)
|
|
18
|
+
* - components/custom/ui/** directory (third-party generated files)
|
|
19
|
+
* - components/shared/** directory (shared utility components)
|
|
20
|
+
* @module eslint-plugin-component-structure/rules/single-component-per-file
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
meta: {
|
|
25
|
+
type: "problem",
|
|
26
|
+
docs: {
|
|
27
|
+
description:
|
|
28
|
+
"Enforce exactly one React component per View or Container file",
|
|
29
|
+
category: "Best Practices",
|
|
30
|
+
recommended: true,
|
|
31
|
+
},
|
|
32
|
+
schema: [],
|
|
33
|
+
messages: {
|
|
34
|
+
multipleComponents:
|
|
35
|
+
"Only one React component is allowed per file. Found '{{componentName}}' in addition to '{{firstComponentName}}'. Extract '{{componentName}}' to a separate file.",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
create(context) {
|
|
40
|
+
const filename = context.getFilename();
|
|
41
|
+
const normalizedPath = filename.replace(/\\/g, "/");
|
|
42
|
+
|
|
43
|
+
// Only check View and Container files
|
|
44
|
+
const isViewOrContainer =
|
|
45
|
+
filename.endsWith("View.tsx") ||
|
|
46
|
+
filename.endsWith("View.jsx") ||
|
|
47
|
+
filename.endsWith("Container.tsx") ||
|
|
48
|
+
filename.endsWith("Container.jsx");
|
|
49
|
+
|
|
50
|
+
if (!isViewOrContainer) {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Exclude components/ui/**, components/custom/ui/**, and components/shared/** directories
|
|
55
|
+
if (
|
|
56
|
+
normalizedPath.includes("/components/ui/") ||
|
|
57
|
+
normalizedPath.includes("/components/custom/ui/") ||
|
|
58
|
+
normalizedPath.includes("/components/shared/") ||
|
|
59
|
+
normalizedPath.startsWith("components/ui/") ||
|
|
60
|
+
normalizedPath.startsWith("components/custom/ui/") ||
|
|
61
|
+
normalizedPath.startsWith("components/shared/")
|
|
62
|
+
) {
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check if file is in features/**/components directory or components directory
|
|
67
|
+
const isFeatureComponent =
|
|
68
|
+
normalizedPath.includes("/features/") &&
|
|
69
|
+
normalizedPath.includes("/components/");
|
|
70
|
+
const isComponentsDir =
|
|
71
|
+
normalizedPath.includes("/components/") ||
|
|
72
|
+
normalizedPath.startsWith("components/");
|
|
73
|
+
|
|
74
|
+
if (!isFeatureComponent && !isComponentsDir) {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const state = {
|
|
79
|
+
components: [], // Array of { name, node }
|
|
80
|
+
firstComponent: null,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Recursively checks if an expression contains JSX
|
|
85
|
+
* @param {object} node - AST node to check
|
|
86
|
+
* @returns {boolean} True if expression contains JSX
|
|
87
|
+
*/
|
|
88
|
+
const containsJSX = node => {
|
|
89
|
+
if (!node) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const type = node.type;
|
|
94
|
+
if (type === "JSXElement" || type === "JSXFragment") {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check conditional expressions: condition ? consequent : alternate
|
|
99
|
+
if (type === "ConditionalExpression") {
|
|
100
|
+
return containsJSX(node.consequent) || containsJSX(node.alternate);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check logical expressions: left && right, left || right
|
|
104
|
+
if (type === "LogicalExpression") {
|
|
105
|
+
return containsJSX(node.left) || containsJSX(node.right);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check parenthesized expressions
|
|
109
|
+
if (type === "ParenthesizedExpression") {
|
|
110
|
+
return containsJSX(node.expression);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return false;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Checks if a function returns JSX by examining its body
|
|
118
|
+
* @param {object} node - AST node to check
|
|
119
|
+
* @returns {boolean} True if function returns JSX
|
|
120
|
+
*/
|
|
121
|
+
const returnsJSX = node => {
|
|
122
|
+
if (!node || !node.body) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Handle arrow function with direct JSX return (no block)
|
|
127
|
+
if (node.type === "ArrowFunctionExpression") {
|
|
128
|
+
return containsJSX(node.body);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Handle function with block body
|
|
132
|
+
if (node.body.type === "BlockStatement") {
|
|
133
|
+
const hasJSXReturn = node.body.body.some(statement => {
|
|
134
|
+
return (
|
|
135
|
+
statement.type === "ReturnStatement" &&
|
|
136
|
+
containsJSX(statement.argument)
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (hasJSXReturn) {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return false;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Records a component if it meets all criteria (PascalCase + returns JSX)
|
|
150
|
+
* @param {string} name - Component name
|
|
151
|
+
* @param {object} node - AST node
|
|
152
|
+
* @param {object} functionNode - Function AST node
|
|
153
|
+
*/
|
|
154
|
+
const recordComponent = (name, node, functionNode) => {
|
|
155
|
+
// Check if name is PascalCase
|
|
156
|
+
if (!/^[A-Z]/.test(name)) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check if function returns JSX
|
|
161
|
+
if (!returnsJSX(functionNode)) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// This is a component - record it
|
|
166
|
+
if (state.components.length === 0) {
|
|
167
|
+
state.firstComponent = { name, node };
|
|
168
|
+
}
|
|
169
|
+
state.components.push({ name, node });
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
VariableDeclarator(node) {
|
|
174
|
+
if (node.id.type !== "Identifier") {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const name = node.id.name;
|
|
179
|
+
|
|
180
|
+
// Check for arrow function assignment: const Component = () => <div />
|
|
181
|
+
if (node.init && node.init.type === "ArrowFunctionExpression") {
|
|
182
|
+
const jsxCheck = returnsJSX(node.init);
|
|
183
|
+
if (jsxCheck) {
|
|
184
|
+
recordComponent(name, node, node.init);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check for memo-wrapped component: const Component = memo(() => <div />)
|
|
190
|
+
if (
|
|
191
|
+
node.init &&
|
|
192
|
+
node.init.type === "CallExpression" &&
|
|
193
|
+
((node.init.callee.type === "Identifier" &&
|
|
194
|
+
node.init.callee.name === "memo") ||
|
|
195
|
+
(node.init.callee.type === "MemberExpression" &&
|
|
196
|
+
node.init.callee.object.name === "React" &&
|
|
197
|
+
node.init.callee.property.name === "memo"))
|
|
198
|
+
) {
|
|
199
|
+
const firstArg = node.init.arguments[0];
|
|
200
|
+
if (firstArg && returnsJSX(firstArg)) {
|
|
201
|
+
recordComponent(name, node, firstArg);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check for React.FC typed components: const Component: React.FC = () => <div />
|
|
207
|
+
if (
|
|
208
|
+
node.init &&
|
|
209
|
+
node.init.type === "ArrowFunctionExpression" &&
|
|
210
|
+
node.id.typeAnnotation &&
|
|
211
|
+
returnsJSX(node.init)
|
|
212
|
+
) {
|
|
213
|
+
recordComponent(name, node, node.init);
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
FunctionDeclaration(node) {
|
|
218
|
+
if (!node.id || node.id.type !== "Identifier") {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const name = node.id.name;
|
|
223
|
+
recordComponent(name, node, node);
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
"Program:exit"() {
|
|
227
|
+
// Report all components after the first one
|
|
228
|
+
if (state.components.length > 1) {
|
|
229
|
+
state.components.slice(1).forEach(component => {
|
|
230
|
+
context.report({
|
|
231
|
+
node: component.node,
|
|
232
|
+
messageId: "multipleComponents",
|
|
233
|
+
data: {
|
|
234
|
+
componentName: component.name,
|
|
235
|
+
firstComponentName: state.firstComponent.name,
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
},
|
|
243
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# ESLint Plugin: UI Standards
|
|
2
|
+
|
|
3
|
+
Custom ESLint rules for enforcing UI-related coding standards in React Native applications.
|
|
4
|
+
|
|
5
|
+
## Rules
|
|
6
|
+
|
|
7
|
+
### no-classname-outside-ui
|
|
8
|
+
|
|
9
|
+
Restricts the use of `className` prop to designated UI component directories.
|
|
10
|
+
|
|
11
|
+
#### Rule Details
|
|
12
|
+
|
|
13
|
+
This rule ensures that `className` (used with Tailwind/NativeWind) is only used in reusable UI components. Business components should use semantic props instead of styling classes.
|
|
14
|
+
|
|
15
|
+
**Why this rule exists:**
|
|
16
|
+
- Keeps styling concerns in UI layer components
|
|
17
|
+
- Business components remain style-agnostic
|
|
18
|
+
- Makes component APIs more semantic and maintainable
|
|
19
|
+
- Facilitates design system consistency
|
|
20
|
+
|
|
21
|
+
**Where is className allowed?**
|
|
22
|
+
- `components/ui/` - Core UI components
|
|
23
|
+
- `components/custom/ui/` - Custom UI components
|
|
24
|
+
|
|
25
|
+
#### Examples
|
|
26
|
+
|
|
27
|
+
**Incorrect** (className in business component):
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
// features/user/components/ProfileCard/ProfileCardView.tsx
|
|
31
|
+
const ProfileCardView = ({ user }) => (
|
|
32
|
+
<View className="p-4 bg-white rounded-lg"> {/* className here - NOT allowed */}
|
|
33
|
+
<Text className="text-lg font-bold">{user.name}</Text>
|
|
34
|
+
</View>
|
|
35
|
+
);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Correct** (using UI components with semantic props):
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
// features/user/components/ProfileCard/ProfileCardView.tsx
|
|
42
|
+
import { Card, Heading } from '@/components/ui';
|
|
43
|
+
|
|
44
|
+
const ProfileCardView = ({ user }) => (
|
|
45
|
+
<Card variant="elevated">
|
|
46
|
+
<Heading size="lg">{user.name}</Heading>
|
|
47
|
+
</Card>
|
|
48
|
+
);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Correct** (className in UI component):
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
// components/ui/Card/CardView.tsx
|
|
55
|
+
const CardView = ({ variant, children }) => (
|
|
56
|
+
<View className={cn("rounded-lg", variants[variant])}>
|
|
57
|
+
{children}
|
|
58
|
+
</View>
|
|
59
|
+
);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
#### Configuration
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
// eslint.config.mjs
|
|
66
|
+
{
|
|
67
|
+
rules: {
|
|
68
|
+
'ui-standards/no-classname-outside-ui': ['error', {
|
|
69
|
+
allowedPaths: ['/components/ui/', '/components/custom/ui/']
|
|
70
|
+
}]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Options:**
|
|
76
|
+
|
|
77
|
+
| Option | Type | Default | Description |
|
|
78
|
+
|--------|------|---------|-------------|
|
|
79
|
+
| `allowedPaths` | `string[]` | `['/components/ui/', '/components/custom/ui/']` | Paths where className is allowed |
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
### no-direct-rn-imports
|
|
84
|
+
|
|
85
|
+
Prevents direct imports from `react-native` to encourage use of wrapped UI components.
|
|
86
|
+
|
|
87
|
+
#### Rule Details
|
|
88
|
+
|
|
89
|
+
This rule blocks direct imports from `react-native` in favor of using the project's UI component library. This ensures:
|
|
90
|
+
|
|
91
|
+
- Consistent styling across the app
|
|
92
|
+
- Ability to swap underlying implementations
|
|
93
|
+
- Centralized accessibility handling
|
|
94
|
+
- Design system compliance
|
|
95
|
+
|
|
96
|
+
**What's blocked?**
|
|
97
|
+
- `import { View, Text, ... } from 'react-native'`
|
|
98
|
+
|
|
99
|
+
**What to use instead?**
|
|
100
|
+
- `import { View, Text, ... } from '@/components/ui'`
|
|
101
|
+
|
|
102
|
+
#### Examples
|
|
103
|
+
|
|
104
|
+
**Incorrect:**
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
import { View, Text, TouchableOpacity } from 'react-native';
|
|
108
|
+
|
|
109
|
+
const MyComponent = () => (
|
|
110
|
+
<View>
|
|
111
|
+
<Text>Hello</Text>
|
|
112
|
+
<TouchableOpacity onPress={handlePress}>
|
|
113
|
+
<Text>Click me</Text>
|
|
114
|
+
</TouchableOpacity>
|
|
115
|
+
</View>
|
|
116
|
+
);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Correct:**
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
import { View, Text, Button } from '@/components/ui';
|
|
123
|
+
|
|
124
|
+
const MyComponent = () => (
|
|
125
|
+
<View>
|
|
126
|
+
<Text>Hello</Text>
|
|
127
|
+
<Button onPress={handlePress}>Click me</Button>
|
|
128
|
+
</View>
|
|
129
|
+
);
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
#### Configuration
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
// eslint.config.mjs
|
|
136
|
+
{
|
|
137
|
+
rules: {
|
|
138
|
+
'ui-standards/no-direct-rn-imports': 'error'
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Allowed directories:**
|
|
144
|
+
- `components/ui/` - UI wrappers need to import from react-native
|
|
145
|
+
- `components/custom/ui/` - Custom UI components
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Installation
|
|
150
|
+
|
|
151
|
+
This plugin is installed locally as a file dependency:
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"devDependencies": {
|
|
156
|
+
"eslint-plugin-ui-standards": "file:./eslint-plugin-ui-standards"
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Usage with ESLint 9 Flat Config
|
|
162
|
+
|
|
163
|
+
```javascript
|
|
164
|
+
// eslint.config.mjs
|
|
165
|
+
import uiStandardsPlugin from './eslint-plugin-ui-standards/index.js';
|
|
166
|
+
|
|
167
|
+
export default [
|
|
168
|
+
{
|
|
169
|
+
plugins: {
|
|
170
|
+
'ui-standards': uiStandardsPlugin,
|
|
171
|
+
},
|
|
172
|
+
rules: {
|
|
173
|
+
'ui-standards/no-classname-outside-ui': 'error',
|
|
174
|
+
'ui-standards/no-direct-rn-imports': 'error',
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Contributing
|
|
181
|
+
|
|
182
|
+
When adding new rules:
|
|
183
|
+
|
|
184
|
+
1. Create rule implementation in `rules/`
|
|
185
|
+
2. Add tests in `__tests__/`
|
|
186
|
+
3. Export in `index.js`
|
|
187
|
+
4. Document in this README
|
|
188
|
+
5. Add to ESLint configuration
|
|
189
|
+
|
|
190
|
+
## Version
|
|
191
|
+
|
|
192
|
+
1.0.0
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is managed by Lisa.
|
|
3
|
+
* Do not edit directly — changes will be overwritten on the next `lisa` run.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ESLint plugin for UI standards
|
|
8
|
+
*
|
|
9
|
+
* This plugin enforces UI-related coding standards for React Native components.
|
|
10
|
+
* Supports ESLint 9 flat config format.
|
|
11
|
+
*
|
|
12
|
+
* Rules:
|
|
13
|
+
* - no-classname-outside-ui: Disallows className prop outside UI components
|
|
14
|
+
* - no-direct-rn-imports: Disallows direct React Native imports
|
|
15
|
+
* @module eslint-plugin-ui-standards
|
|
16
|
+
*/
|
|
17
|
+
const noClassnameOutsideUi = require("./rules/no-classname-outside-ui");
|
|
18
|
+
const noDirectRnImports = require("./rules/no-direct-rn-imports");
|
|
19
|
+
|
|
20
|
+
const plugin = {
|
|
21
|
+
meta: {
|
|
22
|
+
name: "eslint-plugin-ui-standards",
|
|
23
|
+
version: "1.0.0",
|
|
24
|
+
},
|
|
25
|
+
rules: {
|
|
26
|
+
"no-classname-outside-ui": noClassnameOutsideUi,
|
|
27
|
+
"no-direct-rn-imports": noDirectRnImports,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
module.exports = plugin;
|