@codyswann/lisa 1.55.3 → 1.56.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/all/deletions.json +0 -1
- package/cdk/copy-overwrite/tsconfig.json +3 -0
- package/expo/copy-overwrite/tsconfig.json +7 -0
- package/expo/deletions.json +1 -0
- package/nestjs/copy-overwrite/tsconfig.json +6 -0
- package/package.json +6 -3
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/skills/reduce-complexity/SKILL.md +251 -0
- package/plugins/lisa-expo/skills/reduce-complexity/references/extraction-strategies.md +456 -0
- package/plugins/lisa-expo/skills/reduce-complexity/references/refactoring-patterns.md +557 -0
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/src/expo/skills/reduce-complexity/SKILL.md +251 -0
- package/plugins/src/expo/skills/reduce-complexity/references/extraction-strategies.md +456 -0
- package/plugins/src/expo/skills/reduce-complexity/references/refactoring-patterns.md +557 -0
- package/tsconfig/base.json +1 -2
- package/tsconfig/expo.json +0 -5
- package/tsconfig/nestjs.json +1 -5
- package/typescript/copy-overwrite/tsconfig.json +3 -0
- package/typescript/package-lisa/package.lisa.json +2 -1
- package/scripts/strip-workspaces-for-pack.sh +0 -20
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
# Extraction Strategies
|
|
2
|
+
|
|
3
|
+
This reference provides detailed patterns for extracting code to reduce cognitive complexity.
|
|
4
|
+
|
|
5
|
+
## Strategy 1: Helper Functions in View Files
|
|
6
|
+
|
|
7
|
+
Helper functions are the simplest extraction strategy. Use them for rendering logic that doesn't need its own component lifecycle.
|
|
8
|
+
|
|
9
|
+
### Pattern: Render Helper with Props Object
|
|
10
|
+
|
|
11
|
+
Always use a props object (not positional arguments) for type safety and clarity:
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
/**
|
|
15
|
+
* Renders a section header with title and count.
|
|
16
|
+
* @param props - Helper function properties
|
|
17
|
+
* @param props.title - Section title text
|
|
18
|
+
* @param props.count - Item count to display
|
|
19
|
+
* @param props.colors - Theme colors object
|
|
20
|
+
*/
|
|
21
|
+
function renderSectionHeader(props: {
|
|
22
|
+
readonly title: string;
|
|
23
|
+
readonly count: number;
|
|
24
|
+
readonly colors: KanbanColors;
|
|
25
|
+
}) {
|
|
26
|
+
const { title, count, colors } = props;
|
|
27
|
+
return (
|
|
28
|
+
<HStack className="items-center justify-between py-2">
|
|
29
|
+
<Text
|
|
30
|
+
className="text-xs font-semibold uppercase tracking-wider"
|
|
31
|
+
style={{ color: colors.textMuted }}
|
|
32
|
+
>
|
|
33
|
+
{title}
|
|
34
|
+
</Text>
|
|
35
|
+
<Text className="text-xs" style={{ color: colors.textSecondary }}>
|
|
36
|
+
({count})
|
|
37
|
+
</Text>
|
|
38
|
+
</HStack>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Pattern: Conditional Render Helper
|
|
44
|
+
|
|
45
|
+
For sections that render conditionally based on data:
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
/**
|
|
49
|
+
* Renders the empty state when no items exist.
|
|
50
|
+
* @param props - Helper function properties
|
|
51
|
+
* @param props.message - Empty state message
|
|
52
|
+
* @param props.colors - Theme colors object
|
|
53
|
+
*/
|
|
54
|
+
function renderEmptyState(props: {
|
|
55
|
+
readonly message: string;
|
|
56
|
+
readonly colors: KanbanColors;
|
|
57
|
+
}) {
|
|
58
|
+
const { message, colors } = props;
|
|
59
|
+
return (
|
|
60
|
+
<Box
|
|
61
|
+
className="items-center justify-center p-8"
|
|
62
|
+
style={{ backgroundColor: colors.cardBackground }}
|
|
63
|
+
>
|
|
64
|
+
<Text style={{ color: colors.textMuted }}>{message}</Text>
|
|
65
|
+
</Box>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Usage in View
|
|
70
|
+
const ListView = ({ items, isEmpty, colors }: Props) => (
|
|
71
|
+
<Box>
|
|
72
|
+
{isEmpty
|
|
73
|
+
? renderEmptyState({ message: "No items found", colors })
|
|
74
|
+
: items.map(item => <Item key={item.id} item={item} />)}
|
|
75
|
+
</Box>
|
|
76
|
+
);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Pattern: List Item Render Helper
|
|
80
|
+
|
|
81
|
+
For repeated item rendering with complex styling:
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
/**
|
|
85
|
+
* Renders a selectable chip/pill item.
|
|
86
|
+
* @param props - Helper function properties
|
|
87
|
+
* @param props.label - Display text for the chip
|
|
88
|
+
* @param props.isSelected - Whether the chip is currently selected
|
|
89
|
+
* @param props.colors - Theme colors for styling
|
|
90
|
+
* @param props.onPress - Callback when chip is pressed
|
|
91
|
+
*/
|
|
92
|
+
function renderChip(props: {
|
|
93
|
+
readonly label: string;
|
|
94
|
+
readonly isSelected: boolean;
|
|
95
|
+
readonly colors: KanbanColors;
|
|
96
|
+
readonly onPress: () => void;
|
|
97
|
+
}) {
|
|
98
|
+
const { label, isSelected, colors, onPress } = props;
|
|
99
|
+
return (
|
|
100
|
+
<Pressable
|
|
101
|
+
onPress={onPress}
|
|
102
|
+
style={{
|
|
103
|
+
paddingHorizontal: 10,
|
|
104
|
+
paddingVertical: 5,
|
|
105
|
+
borderRadius: 6,
|
|
106
|
+
backgroundColor: isSelected ? colors.primary : colors.cardBackground,
|
|
107
|
+
borderWidth: 1,
|
|
108
|
+
borderColor: isSelected ? colors.primary : colors.border,
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
<Text
|
|
112
|
+
className="text-xs font-medium"
|
|
113
|
+
style={{ color: isSelected ? "#FFFFFF" : colors.textSecondary }}
|
|
114
|
+
>
|
|
115
|
+
{label}
|
|
116
|
+
</Text>
|
|
117
|
+
</Pressable>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### When NOT to Use Helper Functions
|
|
123
|
+
|
|
124
|
+
Do not use helper functions when:
|
|
125
|
+
|
|
126
|
+
1. **The helper would need hooks** - Extract as full component instead
|
|
127
|
+
2. **The helper is used in multiple files** - Extract as shared component
|
|
128
|
+
3. **The helper has complex event handling** - Container should handle this
|
|
129
|
+
4. **The helper exceeds 30 lines** - Consider full component extraction
|
|
130
|
+
|
|
131
|
+
## Strategy 2: Pre-compute in Container
|
|
132
|
+
|
|
133
|
+
Move complexity from View to Container by pre-computing values.
|
|
134
|
+
|
|
135
|
+
### Pattern: Selection State Pre-computation
|
|
136
|
+
|
|
137
|
+
**Before (complexity in View):**
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
// View has repeated .includes() checks
|
|
141
|
+
<Pressable
|
|
142
|
+
style={{
|
|
143
|
+
backgroundColor: filters.positions.includes(position)
|
|
144
|
+
? colors.primary
|
|
145
|
+
: colors.cardBackground,
|
|
146
|
+
}}
|
|
147
|
+
/>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**After (pre-computed in Container):**
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
// Container
|
|
154
|
+
const positionItems = useMemo(
|
|
155
|
+
() =>
|
|
156
|
+
availablePositions.map(position => ({
|
|
157
|
+
value: position,
|
|
158
|
+
label: position,
|
|
159
|
+
isSelected: filters.positions.includes(position),
|
|
160
|
+
})),
|
|
161
|
+
[availablePositions, filters.positions]
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// View - simple prop access
|
|
165
|
+
{
|
|
166
|
+
positionItems.map(({ value, label, isSelected }) => (
|
|
167
|
+
<Chip
|
|
168
|
+
key={value}
|
|
169
|
+
label={label}
|
|
170
|
+
isSelected={isSelected}
|
|
171
|
+
onPress={() => onPositionToggle(value)}
|
|
172
|
+
/>
|
|
173
|
+
));
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Pattern: View State Enumeration
|
|
178
|
+
|
|
179
|
+
**Before (nested ternaries):**
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
{
|
|
183
|
+
isLoading ? (
|
|
184
|
+
<Spinner />
|
|
185
|
+
) : error ? (
|
|
186
|
+
<ErrorView error={error} />
|
|
187
|
+
) : data.length === 0 ? (
|
|
188
|
+
<EmptyState />
|
|
189
|
+
) : (
|
|
190
|
+
<DataList data={data} />
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**After (enumerated state):**
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
// Container
|
|
199
|
+
type ViewState = "loading" | "error" | "empty" | "ready";
|
|
200
|
+
|
|
201
|
+
const viewState = useMemo((): ViewState => {
|
|
202
|
+
if (isLoading) return "loading";
|
|
203
|
+
if (error) return "error";
|
|
204
|
+
if (data.length === 0) return "empty";
|
|
205
|
+
return "ready";
|
|
206
|
+
}, [isLoading, error, data.length]);
|
|
207
|
+
|
|
208
|
+
// View - switch or map
|
|
209
|
+
const ComponentView = ({ viewState, data, error }: Props) => (
|
|
210
|
+
<Box>
|
|
211
|
+
{viewState === "loading" && <Spinner />}
|
|
212
|
+
{viewState === "error" && <ErrorView error={error} />}
|
|
213
|
+
{viewState === "empty" && <EmptyState />}
|
|
214
|
+
{viewState === "ready" && <DataList data={data} />}
|
|
215
|
+
</Box>
|
|
216
|
+
);
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Pattern: Style Pre-computation
|
|
220
|
+
|
|
221
|
+
**Before (inline style objects create complexity and perf issues):**
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
<Box
|
|
225
|
+
style={{
|
|
226
|
+
backgroundColor: isDark ? colors.dark.bg : colors.light.bg,
|
|
227
|
+
padding: isCompact ? 8 : 16,
|
|
228
|
+
borderRadius: isRounded ? 12 : 0,
|
|
229
|
+
}}
|
|
230
|
+
/>
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**After (computed style in Container):**
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
// Container
|
|
237
|
+
const boxStyle = useMemo(
|
|
238
|
+
() => ({
|
|
239
|
+
backgroundColor: isDark ? colors.dark.bg : colors.light.bg,
|
|
240
|
+
padding: isCompact ? 8 : 16,
|
|
241
|
+
borderRadius: isRounded ? 12 : 0,
|
|
242
|
+
}),
|
|
243
|
+
[isDark, isCompact, isRounded, colors]
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// View
|
|
247
|
+
<Box style={boxStyle} />;
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Strategy 3: Full Component Extraction
|
|
251
|
+
|
|
252
|
+
When helper functions aren't sufficient, extract a full Container/View component.
|
|
253
|
+
|
|
254
|
+
### Decision Checklist
|
|
255
|
+
|
|
256
|
+
Extract as full component when 3+ of these apply:
|
|
257
|
+
|
|
258
|
+
- [ ] Pattern repeats in 2+ files
|
|
259
|
+
- [ ] Section needs its own state
|
|
260
|
+
- [ ] Section has 4+ props
|
|
261
|
+
- [ ] Section has callbacks that could be simplified
|
|
262
|
+
- [ ] Section represents a meaningful domain concept
|
|
263
|
+
- [ ] Section could be tested independently
|
|
264
|
+
|
|
265
|
+
### Extraction Steps
|
|
266
|
+
|
|
267
|
+
1. **Identify the repeated/complex pattern**
|
|
268
|
+
2. **Define the component's props interface**
|
|
269
|
+
3. **Create directory structure:**
|
|
270
|
+
```text
|
|
271
|
+
ComponentName/
|
|
272
|
+
├── ComponentNameContainer.tsx
|
|
273
|
+
├── ComponentNameView.tsx
|
|
274
|
+
└── index.tsx
|
|
275
|
+
```
|
|
276
|
+
4. **Write Container with logic/state**
|
|
277
|
+
5. **Write View with pure rendering**
|
|
278
|
+
6. **Update parent to use new component**
|
|
279
|
+
7. **Write tests for new component**
|
|
280
|
+
|
|
281
|
+
### Example: Extracting FilterChipList
|
|
282
|
+
|
|
283
|
+
**Before (in FilterModalView.tsx):**
|
|
284
|
+
|
|
285
|
+
```tsx
|
|
286
|
+
{
|
|
287
|
+
/* Positions - repeated pattern */
|
|
288
|
+
}
|
|
289
|
+
{
|
|
290
|
+
availablePositions.length > 0 && (
|
|
291
|
+
<VStack className="gap-3">
|
|
292
|
+
<Text style={{ color: colors.textMuted }}>Positions</Text>
|
|
293
|
+
<HStack className="flex-wrap gap-2">
|
|
294
|
+
{availablePositions.map(position => (
|
|
295
|
+
<Pressable
|
|
296
|
+
key={position}
|
|
297
|
+
onPress={() => onPositionToggle(position)}
|
|
298
|
+
style={{
|
|
299
|
+
backgroundColor: filters.positions.includes(position)
|
|
300
|
+
? colors.primary
|
|
301
|
+
: colors.cardBackground,
|
|
302
|
+
// ... more styles
|
|
303
|
+
}}
|
|
304
|
+
>
|
|
305
|
+
<Text>{position}</Text>
|
|
306
|
+
</Pressable>
|
|
307
|
+
))}
|
|
308
|
+
</HStack>
|
|
309
|
+
</VStack>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
{
|
|
314
|
+
/* Tags - same pattern with slight variation */
|
|
315
|
+
}
|
|
316
|
+
{
|
|
317
|
+
availableTags.length > 0 && (
|
|
318
|
+
<VStack className="gap-3">
|
|
319
|
+
<Text style={{ color: colors.textMuted }}>Tags</Text>
|
|
320
|
+
<HStack className="flex-wrap gap-2">
|
|
321
|
+
{availableTags.map(tag => (
|
|
322
|
+
<Pressable
|
|
323
|
+
key={tag.id}
|
|
324
|
+
onPress={() => onTagToggle(tag.id)}
|
|
325
|
+
// ... same pattern
|
|
326
|
+
>
|
|
327
|
+
<Text>{tag.name}</Text>
|
|
328
|
+
</Pressable>
|
|
329
|
+
))}
|
|
330
|
+
</HStack>
|
|
331
|
+
</VStack>
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**After (FilterChipList component):**
|
|
337
|
+
|
|
338
|
+
```tsx
|
|
339
|
+
// FilterChipListContainer.tsx
|
|
340
|
+
interface FilterChipListProps {
|
|
341
|
+
readonly title: string;
|
|
342
|
+
readonly items: readonly { id: string; label: string; color?: string }[];
|
|
343
|
+
readonly selectedIds: readonly string[];
|
|
344
|
+
readonly colors: KanbanColors;
|
|
345
|
+
readonly onToggle: (id: string) => void;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const FilterChipListContainer = ({
|
|
349
|
+
title,
|
|
350
|
+
items,
|
|
351
|
+
selectedIds,
|
|
352
|
+
colors,
|
|
353
|
+
onToggle,
|
|
354
|
+
}: FilterChipListProps) => {
|
|
355
|
+
const chipItems = useMemo(
|
|
356
|
+
() =>
|
|
357
|
+
items.map(item => ({
|
|
358
|
+
...item,
|
|
359
|
+
isSelected: selectedIds.includes(item.id),
|
|
360
|
+
})),
|
|
361
|
+
[items, selectedIds]
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
const handleToggle = useCallback(
|
|
365
|
+
(id: string) => () => onToggle(id),
|
|
366
|
+
[onToggle]
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
return (
|
|
370
|
+
<FilterChipListView
|
|
371
|
+
title={title}
|
|
372
|
+
items={chipItems}
|
|
373
|
+
colors={colors}
|
|
374
|
+
onToggle={handleToggle}
|
|
375
|
+
/>
|
|
376
|
+
);
|
|
377
|
+
};
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
```tsx
|
|
381
|
+
// FilterChipListView.tsx
|
|
382
|
+
interface FilterChipListViewProps {
|
|
383
|
+
readonly title: string;
|
|
384
|
+
readonly items: readonly {
|
|
385
|
+
id: string;
|
|
386
|
+
label: string;
|
|
387
|
+
color?: string;
|
|
388
|
+
isSelected: boolean;
|
|
389
|
+
}[];
|
|
390
|
+
readonly colors: KanbanColors;
|
|
391
|
+
readonly onToggle: (id: string) => () => void;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const FilterChipListView = ({
|
|
395
|
+
title,
|
|
396
|
+
items,
|
|
397
|
+
colors,
|
|
398
|
+
onToggle,
|
|
399
|
+
}: FilterChipListViewProps) => (
|
|
400
|
+
<VStack className="gap-3">
|
|
401
|
+
<Text
|
|
402
|
+
className="text-xs font-semibold uppercase tracking-wider"
|
|
403
|
+
style={{ color: colors.textMuted }}
|
|
404
|
+
>
|
|
405
|
+
{title}
|
|
406
|
+
</Text>
|
|
407
|
+
<HStack className="flex-wrap gap-2">
|
|
408
|
+
{items.map(({ id, label, color, isSelected }) => (
|
|
409
|
+
<Pressable
|
|
410
|
+
key={id}
|
|
411
|
+
onPress={onToggle(id)}
|
|
412
|
+
style={{
|
|
413
|
+
paddingHorizontal: 10,
|
|
414
|
+
paddingVertical: 5,
|
|
415
|
+
borderRadius: 6,
|
|
416
|
+
backgroundColor: isSelected
|
|
417
|
+
? (color ?? colors.primary)
|
|
418
|
+
: colors.cardBackground,
|
|
419
|
+
borderWidth: 1,
|
|
420
|
+
borderColor: isSelected ? (color ?? colors.primary) : colors.border,
|
|
421
|
+
}}
|
|
422
|
+
>
|
|
423
|
+
<Text
|
|
424
|
+
className="text-xs font-medium"
|
|
425
|
+
style={{ color: isSelected ? "#FFFFFF" : colors.textSecondary }}
|
|
426
|
+
>
|
|
427
|
+
{label}
|
|
428
|
+
</Text>
|
|
429
|
+
</Pressable>
|
|
430
|
+
))}
|
|
431
|
+
</HStack>
|
|
432
|
+
</VStack>
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
FilterChipListView.displayName = "FilterChipListView";
|
|
436
|
+
export default memo(FilterChipListView);
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
**Usage in parent:**
|
|
440
|
+
|
|
441
|
+
```tsx
|
|
442
|
+
<FilterChipList
|
|
443
|
+
title="Positions"
|
|
444
|
+
items={positionItems}
|
|
445
|
+
selectedIds={filters.positions}
|
|
446
|
+
colors={colors}
|
|
447
|
+
onToggle={onPositionToggle}
|
|
448
|
+
/>
|
|
449
|
+
<FilterChipList
|
|
450
|
+
title="Tags"
|
|
451
|
+
items={tagItems}
|
|
452
|
+
selectedIds={filters.tags}
|
|
453
|
+
colors={colors}
|
|
454
|
+
onToggle={onTagToggle}
|
|
455
|
+
/>
|
|
456
|
+
```
|