@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.
Files changed (21) hide show
  1. package/node_modules/@codyswann/eslint-plugin-code-organization/README.md +149 -0
  2. package/node_modules/@codyswann/eslint-plugin-code-organization/__tests__/enforce-statement-order.test.js +473 -0
  3. package/node_modules/@codyswann/eslint-plugin-code-organization/index.js +28 -0
  4. package/node_modules/@codyswann/eslint-plugin-code-organization/package.json +10 -0
  5. package/node_modules/@codyswann/eslint-plugin-code-organization/rules/enforce-statement-order.js +162 -0
  6. package/node_modules/@codyswann/eslint-plugin-component-structure/README.md +234 -0
  7. package/node_modules/@codyswann/eslint-plugin-component-structure/__tests__/plugin-index.test.js +89 -0
  8. package/node_modules/@codyswann/eslint-plugin-component-structure/__tests__/require-memo-in-view.test.js +201 -0
  9. package/node_modules/@codyswann/eslint-plugin-component-structure/__tests__/single-component-per-file.test.js +294 -0
  10. package/node_modules/@codyswann/eslint-plugin-component-structure/index.js +37 -0
  11. package/node_modules/@codyswann/eslint-plugin-component-structure/package.json +10 -0
  12. package/node_modules/@codyswann/eslint-plugin-component-structure/rules/enforce-component-structure.js +235 -0
  13. package/node_modules/@codyswann/eslint-plugin-component-structure/rules/no-return-in-view.js +96 -0
  14. package/node_modules/@codyswann/eslint-plugin-component-structure/rules/require-memo-in-view.js +183 -0
  15. package/node_modules/@codyswann/eslint-plugin-component-structure/rules/single-component-per-file.js +243 -0
  16. package/node_modules/@codyswann/eslint-plugin-ui-standards/README.md +192 -0
  17. package/node_modules/@codyswann/eslint-plugin-ui-standards/index.js +31 -0
  18. package/node_modules/@codyswann/eslint-plugin-ui-standards/package.json +10 -0
  19. package/node_modules/@codyswann/eslint-plugin-ui-standards/rules/no-classname-outside-ui.js +56 -0
  20. package/node_modules/@codyswann/eslint-plugin-ui-standards/rules/no-direct-rn-imports.js +60 -0
  21. package/package.json +6 -1
@@ -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
+ };
@@ -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;
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "@codyswann/eslint-plugin-ui-standards",
3
+ "version": "1.0.0",
4
+ "description": "ESLint plugin for UI component standards",
5
+ "main": "index.js",
6
+ "publishConfig": { "access": "public" },
7
+ "peerDependencies": {
8
+ "eslint": ">=9.0.0"
9
+ }
10
+ }