@codyswann/lisa 4.33.3 → 4.33.5
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/dist/configs/eslint/expo.d.ts.map +1 -1
- package/dist/configs/eslint/expo.js +4 -7
- package/dist/configs/eslint/expo.js.map +1 -1
- package/dist/configs/eslint/view-gate.d.ts +42 -0
- package/dist/configs/eslint/view-gate.d.ts.map +1 -0
- package/dist/configs/eslint/view-gate.js +110 -0
- package/dist/configs/eslint/view-gate.js.map +1 -0
- package/dist/configs/repo-scan.d.ts +25 -0
- package/dist/configs/repo-scan.d.ts.map +1 -0
- package/dist/configs/repo-scan.js +83 -0
- package/dist/configs/repo-scan.js.map +1 -0
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +20 -7
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/eslint-plugin-component-structure/README.md +85 -2
- package/eslint-plugin-component-structure/__tests__/no-hooks-in-view.test.js +229 -0
- package/eslint-plugin-component-structure/__tests__/no-return-in-view.test.js +185 -0
- package/eslint-plugin-component-structure/__tests__/plugin-index.test.js +8 -1
- package/eslint-plugin-component-structure/index.js +4 -1
- package/eslint-plugin-component-structure/lib/view-file-scope.js +62 -0
- package/eslint-plugin-component-structure/rules/no-hooks-in-view.js +101 -0
- package/eslint-plugin-component-structure/rules/no-return-in-view.js +133 -62
- package/expo/copy-overwrite/eslint.expo.ts +88 -1
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/skills/container-view-pattern/SKILL.md +80 -24
- package/plugins/lisa-expo/.codex-plugin/skills/container-view-pattern/scripts/validate_component.py +166 -43
- package/plugins/lisa-expo/skills/container-view-pattern/SKILL.md +80 -24
- package/plugins/lisa-expo/skills/container-view-pattern/scripts/validate_component.py +166 -43
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-agy/skills/container-view-pattern/SKILL.md +80 -24
- package/plugins/lisa-expo-agy/skills/container-view-pattern/scripts/validate_component.py +166 -43
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/skills/container-view-pattern/SKILL.md +80 -24
- package/plugins/lisa-expo-copilot/skills/container-view-pattern/scripts/validate_component.py +166 -43
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/skills/container-view-pattern/SKILL.md +80 -24
- package/plugins/lisa-expo-cursor/skills/container-view-pattern/scripts/validate_component.py +166 -43
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/expo/skills/container-view-pattern/SKILL.md +80 -24
- package/plugins/src/expo/skills/container-view-pattern/scripts/validate_component.py +166 -43
|
@@ -10,11 +10,29 @@ This script validates that a component directory follows the Container/View patt
|
|
|
10
10
|
- Has index.tsx with correct export
|
|
11
11
|
- View uses memo() wrapper
|
|
12
12
|
- View has displayName
|
|
13
|
-
- View
|
|
13
|
+
- View is an arrow function with an expression body (declaration form is a
|
|
14
|
+
violation, not an alternative spelling)
|
|
15
|
+
- View calls no hooks — matched by SHAPE (`use` + uppercase), never by a list of
|
|
16
|
+
names, because the call that motivated this check was a project-local custom
|
|
17
|
+
hook that no list would have contained
|
|
18
|
+
|
|
19
|
+
One check reports as an ADVISORY rather than an error: "Container renders JSX
|
|
20
|
+
besides the View". It is a whole-file text scan that cannot tell a returned JSX
|
|
21
|
+
tree from JSX inside a callback, so a Container holding a legitimate `renderItem`
|
|
22
|
+
trips it — and moving a `renderItem` out of a View is one of the sanctioned fixes
|
|
23
|
+
for the View hook ban. It prints and does not fail.
|
|
24
|
+
|
|
25
|
+
This is a fast pre-check, not the gate. ESLint reads the AST of every file;
|
|
26
|
+
this reads one directory with regexes. When they disagree, ESLint is right.
|
|
14
27
|
|
|
15
28
|
Usage:
|
|
16
29
|
python3 validate_component.py <path-to-component-directory>
|
|
17
30
|
|
|
31
|
+
The script ships inside the Lisa plugin, not in a consumer's `.claude/skills/`,
|
|
32
|
+
so invoke it through the plugin root:
|
|
33
|
+
|
|
34
|
+
python3 "${CLAUDE_PLUGIN_ROOT:-node_modules/@codyswann/lisa/plugins/lisa-expo}/skills/container-view-pattern/scripts/validate_component.py" <path>
|
|
35
|
+
|
|
18
36
|
Example:
|
|
19
37
|
python3 validate_component.py features/player-kanban/components/AddColumnButton
|
|
20
38
|
"""
|
|
@@ -25,21 +43,118 @@ import sys
|
|
|
25
43
|
from pathlib import Path
|
|
26
44
|
|
|
27
45
|
|
|
28
|
-
|
|
46
|
+
HOOK_CALL = re.compile(r"\b(use[A-Z]\w*)\s*\(")
|
|
47
|
+
"""Every hook call, by SHAPE.
|
|
48
|
+
|
|
49
|
+
`use` followed by an uppercase letter is React's own definition of a hook name,
|
|
50
|
+
so this matches `useState`, `useMemo`, and every project-local custom hook
|
|
51
|
+
alike. It is a shape and never a list: a list is what let
|
|
52
|
+
`useCreateNoteQuickActionEnabled()` through.
|
|
53
|
+
|
|
54
|
+
It also matches hook calls inside comments and strings. That is the accepted
|
|
55
|
+
cost of a regex pre-check; ESLint's `component-structure/no-hooks-in-view` reads
|
|
56
|
+
the AST and is the gate.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _arrow_body_is_block(source: str, start: int) -> bool:
|
|
61
|
+
"""
|
|
62
|
+
Whether the arrow beginning at `start` has a block body.
|
|
63
|
+
|
|
64
|
+
Walks to the first top-level `=>` instead of matching a parameter list with
|
|
65
|
+
`\\([^)]*\\)`, which fails on any nested paren — a destructured parameter with
|
|
66
|
+
a default, a type annotation carrying a function type, or a parenthesised
|
|
67
|
+
union.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
source: The View file's text.
|
|
71
|
+
start: Index just past the `const <Name>View` binding.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
True when the arrow's body opens with `{`.
|
|
75
|
+
"""
|
|
76
|
+
depth = 0
|
|
77
|
+
index = start
|
|
78
|
+
while index < len(source) - 1:
|
|
79
|
+
char = source[index]
|
|
80
|
+
if char in "([{":
|
|
81
|
+
depth += 1
|
|
82
|
+
elif char in ")]}":
|
|
83
|
+
depth -= 1
|
|
84
|
+
elif char == ";" and depth == 0:
|
|
85
|
+
return False
|
|
86
|
+
elif depth == 0 and source[index : index + 2] == "=>":
|
|
87
|
+
rest = source[index + 2 :].lstrip()
|
|
88
|
+
return rest.startswith("{")
|
|
89
|
+
index += 1
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def view_form_errors(component_name: str, view_content: str) -> list[str]:
|
|
94
|
+
"""
|
|
95
|
+
Errors about the FORM of the View component.
|
|
96
|
+
|
|
97
|
+
The requirement is an arrow function with an expression body. A function
|
|
98
|
+
declaration cannot have one, so declaration form is banned by construction
|
|
99
|
+
rather than by scanning for the word "function" — which would fire on JSDoc
|
|
100
|
+
prose, on local render helpers, and on disable-comment justifications, none
|
|
101
|
+
of which are the View component.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
component_name: The component directory's name, without the View suffix.
|
|
105
|
+
view_content: The View file's text.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Zero or one error message.
|
|
109
|
+
"""
|
|
110
|
+
name = f"{component_name}View"
|
|
111
|
+
expected = f"const {name} = (props) => (...)"
|
|
112
|
+
|
|
113
|
+
if re.search(rf"\bfunction\s+{re.escape(name)}\b", view_content):
|
|
114
|
+
return [
|
|
115
|
+
f"View must be an arrow function with an expression body: {expected}. "
|
|
116
|
+
"A function declaration cannot have one, so it always carries a statement list."
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
function_expression = re.search(
|
|
120
|
+
rf"\bconst\s+{re.escape(name)}\b[^=]*=\s*function\b", view_content
|
|
121
|
+
)
|
|
122
|
+
if function_expression:
|
|
123
|
+
return [
|
|
124
|
+
f"View must be an arrow function with an expression body: {expected}. "
|
|
125
|
+
"A function expression cannot have one."
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
binding = re.search(rf"\bconst\s+{re.escape(name)}\b", view_content)
|
|
129
|
+
if binding and _arrow_body_is_block(view_content, binding.end()):
|
|
130
|
+
return [
|
|
131
|
+
"View should use arrow function shorthand: () => (...) instead of () => { return (...) }"
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
return []
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def validate_component(component_path: str) -> tuple[bool, list[str], list[str]]:
|
|
29
138
|
"""
|
|
30
139
|
Validate a component directory follows Container/View pattern.
|
|
31
140
|
|
|
141
|
+
Errors and advisories are separated because one check here cannot decide
|
|
142
|
+
what it reports on — see the Container JSX scan below. A check that cannot
|
|
143
|
+
tell a violation from a legitimate shape must say so rather than fail the
|
|
144
|
+
run; a confident wrong verdict is worse than a flagged uncertainty.
|
|
145
|
+
|
|
32
146
|
Args:
|
|
33
147
|
component_path: Path to the component directory
|
|
34
148
|
|
|
35
149
|
Returns:
|
|
36
|
-
Tuple of (is_valid, list of error messages)
|
|
150
|
+
Tuple of (is_valid, list of error messages, list of advisory messages)
|
|
37
151
|
"""
|
|
38
152
|
errors = []
|
|
153
|
+
advisories = []
|
|
39
154
|
component_dir = Path(component_path)
|
|
40
155
|
|
|
41
156
|
if not component_dir.is_dir():
|
|
42
|
-
return False, [f"Path is not a directory: {component_path}"]
|
|
157
|
+
return False, [f"Path is not a directory: {component_path}"], advisories
|
|
43
158
|
|
|
44
159
|
component_name = component_dir.name
|
|
45
160
|
|
|
@@ -92,39 +207,24 @@ def validate_component(component_path: str) -> tuple[bool, list[str]]:
|
|
|
92
207
|
if not re.search(display_name_pattern, view_content):
|
|
93
208
|
errors.append(f"View should have displayName: {component_name}View.displayName = \"{component_name}View\"")
|
|
94
209
|
|
|
95
|
-
# Check
|
|
96
|
-
#
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
# Exclude memo import check
|
|
114
|
-
if hook_pattern == r"\buse[A-Z]\w+\s*\(":
|
|
115
|
-
# More specific check to avoid false positives
|
|
116
|
-
if re.search(r"\buseState\s*\(", view_content):
|
|
117
|
-
errors.append("View should not contain useState - move to Container")
|
|
118
|
-
break
|
|
119
|
-
if re.search(r"\buseEffect\s*\(", view_content):
|
|
120
|
-
errors.append("View should not contain useEffect - move to Container")
|
|
121
|
-
break
|
|
122
|
-
if re.search(r"\buseMemo\s*\(", view_content):
|
|
123
|
-
errors.append("View should not contain useMemo - move to Container")
|
|
124
|
-
break
|
|
125
|
-
if re.search(r"\buseCallback\s*\(", view_content):
|
|
126
|
-
errors.append("View should not contain useCallback - move to Container")
|
|
127
|
-
break
|
|
210
|
+
# Check the View component's form.
|
|
211
|
+
#
|
|
212
|
+
# The old check was `const {name}View = ([^)]*) => {` — arrow-only, and
|
|
213
|
+
# therefore blind to `function {name}View(...) {`, independently
|
|
214
|
+
# reproducing the exact defect the ESLint rule carried. It also broke on
|
|
215
|
+
# any parameter list containing a nested paren.
|
|
216
|
+
errors.extend(view_form_errors(component_name, view_content))
|
|
217
|
+
|
|
218
|
+
# Check for hooks in View (they should be in Container).
|
|
219
|
+
#
|
|
220
|
+
# One generic shape match, deliberately. The previous version listed
|
|
221
|
+
# `\buse[A-Z]\w+\s*\(` first and then narrowed to four hardcoded names
|
|
222
|
+
# and `break`ed, so the general pattern was UNREACHABLE — dead code that
|
|
223
|
+
# read as coverage. A project-local hook such as
|
|
224
|
+
# `useCreateNoteQuickActionEnabled()` matched the pattern that was never
|
|
225
|
+
# evaluated and none of the four that were.
|
|
226
|
+
for hook in sorted(set(HOOK_CALL.findall(view_content))):
|
|
227
|
+
errors.append(f"View should not contain {hook} - move to Container")
|
|
128
228
|
|
|
129
229
|
# Validate Container file
|
|
130
230
|
if container_file.exists():
|
|
@@ -140,14 +240,31 @@ def validate_component(component_path: str) -> tuple[bool, list[str]]:
|
|
|
140
240
|
if not re.search(return_view_pattern, container_content):
|
|
141
241
|
errors.append(f"Container should return <{component_name}View />")
|
|
142
242
|
|
|
143
|
-
#
|
|
144
|
-
#
|
|
243
|
+
# ADVISORY, not an error — and deliberately so.
|
|
244
|
+
#
|
|
245
|
+
# "Container should ONLY render View" is a naive `<([A-Z]\w*)` scan over
|
|
246
|
+
# the ENTIRE container file. It cannot tell a component's returned JSX
|
|
247
|
+
# tree from JSX inside a callback, so a Container holding a perfectly
|
|
248
|
+
# legitimate `renderItem` — `useCallback(({item}) => <Row … />, […])` —
|
|
249
|
+
# trips it.
|
|
250
|
+
#
|
|
251
|
+
# That matters now that Views may not call hooks: moving a renderItem
|
|
252
|
+
# callback to the Container is one of the two sanctioned fixes, and
|
|
253
|
+
# Containers ARE allowed to hold logic, including a callback that
|
|
254
|
+
# returns JSX. Left as an error, this check would reject the very
|
|
255
|
+
# migration the View rules ask for.
|
|
256
|
+
#
|
|
257
|
+
# So it reports and does not fail. Scoping it to the returned tree needs
|
|
258
|
+
# a parser, which a regex pre-check is not; until then, saying plainly
|
|
259
|
+
# what it cannot distinguish beats a confident wrong verdict.
|
|
145
260
|
all_jsx_pattern = r"<([A-Z][a-zA-Z0-9]*)"
|
|
146
261
|
jsx_matches = re.findall(all_jsx_pattern, container_content)
|
|
147
262
|
other_components = [m for m in jsx_matches if m != f"{component_name}View"]
|
|
148
263
|
if other_components:
|
|
149
|
-
|
|
150
|
-
f"Container
|
|
264
|
+
advisories.append(
|
|
265
|
+
f"Container renders JSX besides {component_name}View: {', '.join(sorted(set(other_components)))}. "
|
|
266
|
+
"This is a whole-file text scan and cannot tell a returned JSX tree from JSX inside a callback, "
|
|
267
|
+
"so a legitimate renderItem callback lands here too. Review it; this does not fail validation."
|
|
151
268
|
)
|
|
152
269
|
|
|
153
270
|
# Check for extra files
|
|
@@ -178,7 +295,7 @@ def validate_component(component_path: str) -> tuple[bool, list[str]]:
|
|
|
178
295
|
if not is_allowed and item.is_file():
|
|
179
296
|
errors.append(f"Unexpected file in component directory: {item.name}")
|
|
180
297
|
|
|
181
|
-
return len(errors) == 0, errors
|
|
298
|
+
return len(errors) == 0, errors, advisories
|
|
182
299
|
|
|
183
300
|
|
|
184
301
|
def main():
|
|
@@ -193,7 +310,13 @@ def main():
|
|
|
193
310
|
print(f"Validating component: {component_path}")
|
|
194
311
|
print("-" * 50)
|
|
195
312
|
|
|
196
|
-
is_valid, errors = validate_component(component_path)
|
|
313
|
+
is_valid, errors, advisories = validate_component(component_path)
|
|
314
|
+
|
|
315
|
+
if advisories:
|
|
316
|
+
print("Advisories (reviewed by a human, not a failure):")
|
|
317
|
+
for advisory in advisories:
|
|
318
|
+
print(f" ~ {advisory}")
|
|
319
|
+
print("-" * 50)
|
|
197
320
|
|
|
198
321
|
if is_valid:
|
|
199
322
|
print("Component follows Container/View pattern")
|
|
@@ -43,9 +43,16 @@ The Container/View pattern is **required** in these directories:
|
|
|
43
43
|
Run the skill's generator script for any component type:
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
|
-
python3
|
|
46
|
+
python3 "${CLAUDE_PLUGIN_ROOT:-node_modules/@codyswann/lisa/plugins/lisa-expo}/skills/container-view-pattern/scripts/create_component.py" <type> <name> [feature]
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
The `:-` default is load-bearing. This skill ships inside the Lisa plugin, not in
|
|
50
|
+
the consumer's `.claude/skills/`, and `CLAUDE_PLUGIN_ROOT` is not exported into
|
|
51
|
+
an agent's Bash environment — so a bare `.claude/skills/…` path names a file that
|
|
52
|
+
does not exist in a consumer, and a bare `"${CLAUDE_PLUGIN_ROOT}/…"` expands to
|
|
53
|
+
an absolute `/skills/…` and exits 1. The default resolves from the consumer
|
|
54
|
+
repository root, which is where the script is actually invoked.
|
|
55
|
+
|
|
49
56
|
**Component Types:**
|
|
50
57
|
|
|
51
58
|
| Type | Command | Creates in |
|
|
@@ -71,7 +78,7 @@ ComponentName/
|
|
|
71
78
|
|
|
72
79
|
Container components handle all business logic:
|
|
73
80
|
|
|
74
|
-
1. **Single View render**: Container
|
|
81
|
+
1. **Single View render**: A Container's **returned tree** is only its corresponding View component — no other UI elements beside it. This constrains what the Container renders, not what it holds: a Container may define a callback that returns JSX (a `renderItem` passed down as a prop is the common case), and that is where such a callback belongs once a View may no longer call hooks.
|
|
75
82
|
2. **State management**: Use `useState`, `useReducer`
|
|
76
83
|
3. **Data fetching**: Use GraphQL hooks, API calls
|
|
77
84
|
4. **Memoization**: Wrap all computed values in `useMemo`
|
|
@@ -146,15 +153,33 @@ export default ComponentNameContainer;
|
|
|
146
153
|
|
|
147
154
|
## View Component Requirements
|
|
148
155
|
|
|
149
|
-
View components
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
156
|
+
View components carry **no statements and no hooks**. That is the guarantee, and
|
|
157
|
+
it is deliberately narrower than "pure": `{Date.now()}` and
|
|
158
|
+
`{Math.random() > 0.5 ? … : …}` sit happily inside an expression body and no
|
|
159
|
+
rule below rejects them. Claiming purity here would repeat the defect this
|
|
160
|
+
section was rewritten to fix — see [Validation](#validation).
|
|
161
|
+
|
|
162
|
+
1. **Expression-bodied arrow function**: The View component must be
|
|
163
|
+
`const XView = (props) => (...)`. Not `() => { return (...); }`, and not
|
|
164
|
+
`function XView(props) { ... }` — a function declaration cannot have an
|
|
165
|
+
expression body, so declaration form always carries a statement list. This is
|
|
166
|
+
the requirement; the other two shapes are the two ways of failing it.
|
|
167
|
+
2. **No statements**: The component body is a single JSX expression.
|
|
153
168
|
3. **memo wrapper**: Export with `memo()` for performance optimization
|
|
154
169
|
4. **displayName**: Set `ComponentName.displayName = "ComponentName"`
|
|
155
170
|
5. **Readonly props**: All props should be marked as `readonly`
|
|
156
|
-
6. **No hooks**:
|
|
157
|
-
|
|
171
|
+
6. **No hooks anywhere in the file**: No call of the shape `use` + an uppercase
|
|
172
|
+
letter — `useState`, `useEffect`, `useMemo`, `useCallback`, and every
|
|
173
|
+
project-local custom hook alike. The ban is on the shape, not on a list of
|
|
174
|
+
names: the call that motivated this gate was a project-local hook that no
|
|
175
|
+
name list would have contained. `useMemo` and `useCallback` are caught for
|
|
176
|
+
the same reason — a data hook and a render helper are indistinguishable by
|
|
177
|
+
name, and this document already forbade `useMemo` in a View. There is no
|
|
178
|
+
exemption list, no per-site waiver, and no opt-out flag; a View that calls a
|
|
179
|
+
hook is fixed by moving the hook into the Container.
|
|
180
|
+
7. **No logic**: All conditional rendering should use ternary expressions in JSX.
|
|
181
|
+
Unlike 1, 2 and 6, this one is **not lint-enforced** — see the second table
|
|
182
|
+
under [Validation](#validation).
|
|
158
183
|
|
|
159
184
|
### View Template
|
|
160
185
|
|
|
@@ -207,23 +232,47 @@ export { default } from "./ComponentNameContainer";
|
|
|
207
232
|
|
|
208
233
|
### ESLint Rules
|
|
209
234
|
|
|
210
|
-
|
|
235
|
+
Each row names the numbered requirement it enforces. Every requirement above
|
|
236
|
+
appears in exactly one of the two tables, so a requirement with no rule is
|
|
237
|
+
visible as such rather than implied to be covered.
|
|
238
|
+
|
|
239
|
+
For most of this skill's life the table below listed four rules under the
|
|
240
|
+
heading "the following ESLint rules enforce the pattern" — and none of them
|
|
241
|
+
enforced requirement 6 or 7. That false assurance is why nobody checked, and why
|
|
242
|
+
a declaration-form View with a hook in it passed lint for years.
|
|
243
|
+
|
|
244
|
+
| Rule | Enforces | Description |
|
|
245
|
+
| ------------------------------------------------- | --------- | ----------------------------------------------------------------- |
|
|
246
|
+
| `component-structure/enforce-component-structure` | structure | Validates directory structure and file naming |
|
|
247
|
+
| `component-structure/no-return-in-view` | 1, 2 | View must be an arrow function with an expression body |
|
|
248
|
+
| `component-structure/no-hooks-in-view` | 6 | No `use[A-Z]…()` call anywhere in a View file, by shape not name |
|
|
249
|
+
| `no-restricted-imports` (View files) | 6 | No React hook named-import, and no `**/hooks/**` import |
|
|
250
|
+
| `component-structure/require-memo-in-view` | 3, 4 | Ensures View uses memo and displayName |
|
|
251
|
+
| `component-structure/single-component-per-file` | structure | One component per file |
|
|
211
252
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
253
|
+
`no-hooks-in-view` and `no-restricted-imports` are two independent axes, not one
|
|
254
|
+
duplicated: a hook re-exported through a barrel escapes the import patterns, and
|
|
255
|
+
an import with no call site yet escapes the call matcher.
|
|
256
|
+
|
|
257
|
+
### What lint does NOT enforce
|
|
258
|
+
|
|
259
|
+
| Requirement | Why not |
|
|
260
|
+
| --------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
261
|
+
| 5 — readonly props | A convention on the props interface; carried by review and the templates above. |
|
|
262
|
+
| 7 — no logic beyond ternaries | "Logic" has no AST shape. `{Date.now()}` and `{Math.random() > 0.5 ? … : …}` are legal expression bodies. |
|
|
218
263
|
|
|
219
264
|
### Manual Validation
|
|
220
265
|
|
|
221
266
|
Run the validation script to check a component:
|
|
222
267
|
|
|
223
268
|
```bash
|
|
224
|
-
python3
|
|
269
|
+
python3 "${CLAUDE_PLUGIN_ROOT:-node_modules/@codyswann/lisa/plugins/lisa-expo}/skills/container-view-pattern/scripts/validate_component.py" <path-to-component-directory>
|
|
225
270
|
```
|
|
226
271
|
|
|
272
|
+
The script is a fast pre-check, not the gate: it reads one component directory
|
|
273
|
+
with regexes, while ESLint reads the AST of every file. When they disagree,
|
|
274
|
+
ESLint is right.
|
|
275
|
+
|
|
227
276
|
Run ESLint to check all components:
|
|
228
277
|
|
|
229
278
|
```bash
|
|
@@ -238,7 +287,7 @@ bun run lint
|
|
|
238
287
|
|
|
239
288
|
| Issue | Resolution |
|
|
240
289
|
| ------------------------------------ | ----------------------------------------------------------- |
|
|
241
|
-
|
|
|
290
|
+
| Returning UI elements besides View | Container's returned tree is ONLY the corresponding View |
|
|
242
291
|
| Rendering multiple components | Move all UI to View; Container returns only View |
|
|
243
292
|
| Missing `useMemo` for objects/arrays | Wrap computed values in `useMemo` |
|
|
244
293
|
| Missing `useCallback` for functions | Wrap handlers in `useCallback` |
|
|
@@ -247,13 +296,14 @@ bun run lint
|
|
|
247
296
|
|
|
248
297
|
### View Violations
|
|
249
298
|
|
|
250
|
-
| Issue
|
|
251
|
-
|
|
|
252
|
-
| Using block body `{ return }`
|
|
253
|
-
|
|
|
254
|
-
| Missing `
|
|
255
|
-
|
|
|
256
|
-
| Contains
|
|
299
|
+
| Issue | Resolution |
|
|
300
|
+
| ------------------------------------ | ------------------------------------------------- |
|
|
301
|
+
| Using block body `{ return }` | Convert to arrow shorthand `() => (...)` |
|
|
302
|
+
| Declared as `function XView() {…}` | Convert to `const XView = (props) => (...)` |
|
|
303
|
+
| Missing `memo` wrapper | Add `export default memo(ComponentView)` |
|
|
304
|
+
| Missing `displayName` | Add `ComponentView.displayName = "ComponentView"` |
|
|
305
|
+
| Contains hooks (including custom) | Move hooks to Container |
|
|
306
|
+
| Contains state | Move state to Container |
|
|
257
307
|
|
|
258
308
|
## Extracting Helper Functions
|
|
259
309
|
|
|
@@ -275,6 +325,12 @@ const ComponentView = ({ isLoading, isDark }: Props) => (
|
|
|
275
325
|
);
|
|
276
326
|
```
|
|
277
327
|
|
|
328
|
+
A local render helper may be a function declaration. Requirement 1 is about the
|
|
329
|
+
**View component**, not about the word `function` appearing in the file — which
|
|
330
|
+
is why it is stated as "expression-bodied arrow" rather than as a ban on
|
|
331
|
+
declarations. The helper is still inside a View file, so requirement 6 applies to
|
|
332
|
+
it: it may not call a hook either.
|
|
333
|
+
|
|
278
334
|
## Event Handler Naming Convention
|
|
279
335
|
|
|
280
336
|
- **Container**: Use `handle*` prefix (e.g., `handleSubmit`, `handleClick`)
|