@codyswann/lisa 4.33.4 → 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/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 +18 -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
|
@@ -0,0 +1,101 @@
|
|
|
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 banning hook calls anywhere in a View component file.
|
|
8
|
+
*
|
|
9
|
+
* The Container/View skill has said "View should not contain hooks" since it was
|
|
10
|
+
* written, and for that whole time nothing enforced it — the skill's own table
|
|
11
|
+
* of "ESLint rules that enforce the pattern" listed four rules, none of which
|
|
12
|
+
* looked at a hook. This rule is the missing half.
|
|
13
|
+
*
|
|
14
|
+
* The matcher is a SHAPE — `use` followed by an uppercase letter, called — and
|
|
15
|
+
* deliberately never a list of names. The call that made the gap expensive was a
|
|
16
|
+
* project-local custom hook, so any rule built on an enumerated list would have
|
|
17
|
+
* missed the one that mattered. That is also why there is no exemption list:
|
|
18
|
+
* a custom data hook and a render helper are indistinguishable by name, so
|
|
19
|
+
* `useMemo` and `useCallback` are caught too. The skill already forbids
|
|
20
|
+
* `useMemo` in a View, so exempting it would contradict the document.
|
|
21
|
+
*
|
|
22
|
+
* Scope is the file, not the component body: a hook called from a local render
|
|
23
|
+
* helper still runs during that View's render.
|
|
24
|
+
* @module eslint-plugin-component-structure/rules/no-hooks-in-view
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const { isEnforcedViewFile } = require("../lib/view-file-scope");
|
|
28
|
+
|
|
29
|
+
/** `use` plus an uppercase letter — React's own definition of a hook name. */
|
|
30
|
+
const HOOK_NAME = /^use[A-Z]\w*$/u;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The hook a call expression invokes, if it invokes one.
|
|
34
|
+
*
|
|
35
|
+
* Covers the bare call (`useFlag()`) and the namespaced one
|
|
36
|
+
* (`React.useMemo()`); a computed member access is not a statically knowable
|
|
37
|
+
* name and is left to `no-restricted-imports` and review.
|
|
38
|
+
* @param {object} callee - The callee of a CallExpression.
|
|
39
|
+
* @returns {string|null} The hook name, or null when this is not a hook call.
|
|
40
|
+
*/
|
|
41
|
+
function hookName(callee) {
|
|
42
|
+
if (callee.type === "Identifier" && HOOK_NAME.test(callee.name)) {
|
|
43
|
+
return callee.name;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (
|
|
47
|
+
callee.type === "MemberExpression" &&
|
|
48
|
+
!callee.computed &&
|
|
49
|
+
callee.property.type === "Identifier" &&
|
|
50
|
+
HOOK_NAME.test(callee.property.name)
|
|
51
|
+
) {
|
|
52
|
+
return callee.property.name;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
meta: {
|
|
60
|
+
type: "problem",
|
|
61
|
+
docs: {
|
|
62
|
+
description:
|
|
63
|
+
"Disallow hook calls in View components - hooks belong in the Container",
|
|
64
|
+
category: "Best Practices",
|
|
65
|
+
recommended: true,
|
|
66
|
+
},
|
|
67
|
+
fixable: null,
|
|
68
|
+
schema: [],
|
|
69
|
+
messages: {
|
|
70
|
+
// The message names the escape route on purpose. "No hooks in Views" with
|
|
71
|
+
// no way out is what makes a developer — or an agent — reach for a disable
|
|
72
|
+
// comment, and there is no disable this rule will accept.
|
|
73
|
+
//
|
|
74
|
+
// The Container is always a legitimate home: Containers may hold logic,
|
|
75
|
+
// including a callback that returns JSX. The row-component alternative is
|
|
76
|
+
// named for the `renderItem` shape, where lifting the callback moves JSX
|
|
77
|
+
// rather than removing it — it is an OPTION, never a requirement.
|
|
78
|
+
noHooksInView:
|
|
79
|
+
"View components must not call hooks. Move {{hook}}() into the corresponding Container and pass its result down as a prop — Containers may hold logic, including a callback that returns JSX. If {{hook}}() builds a row (a renderItem or render* callback), extracting that row into its own memoised component is an equally valid alternative.",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
create(context) {
|
|
84
|
+
if (!isEnforcedViewFile(context.getFilename())) {
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
CallExpression(node) {
|
|
90
|
+
const hook = hookName(node.callee);
|
|
91
|
+
if (hook !== null) {
|
|
92
|
+
context.report({
|
|
93
|
+
node,
|
|
94
|
+
messageId: "noHooksInView",
|
|
95
|
+
data: { hook },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
};
|
|
@@ -3,12 +3,85 @@
|
|
|
3
3
|
* Do not edit directly — changes will be overwritten on the next `lisa` run.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* ESLint rule requiring a View component to be an expression-bodied arrow.
|
|
8
|
+
*
|
|
9
|
+
* Stated that way on purpose. The rule used to be stated as "no return
|
|
10
|
+
* statements", registered exactly one `ArrowFunctionExpression` visitor, and
|
|
11
|
+
* nothing anywhere required a View to be an arrow function — so
|
|
12
|
+
* `const XView = (...) => { ... }` was blocked while `function XView(...) { ... }`
|
|
13
|
+
* was never visited and reported nothing, with `require-memo-in-view` still
|
|
14
|
+
* passing because it checks the export. Writing `function` instead of `const`
|
|
15
|
+
* was a silent, complete, permanent opt-out of the only statement gate on Views.
|
|
16
|
+
*
|
|
17
|
+
* An expression body is a property a `FunctionDeclaration` cannot have, so
|
|
18
|
+
* requiring it bans declaration form by construction — no prose scanning, no
|
|
19
|
+
* substring match on the word "function" (which fires on JSDoc, on local render
|
|
20
|
+
* helpers, and on disable-comment justifications), and no false-positive
|
|
21
|
+
* surface.
|
|
22
|
+
*
|
|
23
|
+
* What this rule guarantees is "no statement list". It is NOT a purity gate:
|
|
24
|
+
* `{Date.now()}` and `{Math.random() > 0.5 ? … : …}` sit happily in an
|
|
25
|
+
* expression body. Hooks are the other half and are `no-hooks-in-view`'s job.
|
|
26
|
+
* @module eslint-plugin-component-structure/rules/no-return-in-view
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const { isEnforcedViewFile } = require("../lib/view-file-scope");
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Whether an identifier names a View component by spelling.
|
|
33
|
+
* @param {string} name - The declared identifier.
|
|
34
|
+
* @returns {boolean} True for a PascalCase name containing "View".
|
|
35
|
+
*/
|
|
36
|
+
const isViewComponentName = name =>
|
|
37
|
+
/^[A-Z]/u.test(name) && name.includes("View");
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The binding a function expression or arrow is assigned to, if any.
|
|
41
|
+
* @param {object} node - The function node.
|
|
42
|
+
* @returns {string|null} The declared name, or null when there is none.
|
|
43
|
+
*/
|
|
44
|
+
function declaredName(node) {
|
|
45
|
+
const parent = node.parent;
|
|
46
|
+
return parent &&
|
|
47
|
+
parent.type === "VariableDeclarator" &&
|
|
48
|
+
parent.id.type === "Identifier"
|
|
49
|
+
? parent.id.name
|
|
50
|
+
: null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The identifier a default export ultimately names.
|
|
55
|
+
*
|
|
56
|
+
* Unwraps `memo(X)` and `React.memo(X)` so the component the file actually
|
|
57
|
+
* exports is recognised whatever it is called. Without this the rule would
|
|
58
|
+
* depend on the identifier containing the word "View", and a file named
|
|
59
|
+
* `HomeScreenView.tsx` exporting `HomeScreen` would slip through — the same
|
|
60
|
+
* class of spelling-dependent hole the rule is being fixed for.
|
|
61
|
+
* @param {object} declaration - The `export default` declaration node.
|
|
62
|
+
* @returns {string|null} The exported identifier, or null.
|
|
63
|
+
*/
|
|
64
|
+
function defaultExportedName(declaration) {
|
|
65
|
+
if (declaration.type === "Identifier") {
|
|
66
|
+
return declaration.name;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (declaration.type !== "CallExpression") {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const [firstArgument] = declaration.arguments;
|
|
74
|
+
return firstArgument && firstArgument.type === "Identifier"
|
|
75
|
+
? firstArgument.name
|
|
76
|
+
: null;
|
|
77
|
+
}
|
|
78
|
+
|
|
6
79
|
module.exports = {
|
|
7
80
|
meta: {
|
|
8
81
|
type: "problem",
|
|
9
82
|
docs: {
|
|
10
83
|
description:
|
|
11
|
-
"
|
|
84
|
+
"Require View components to be arrow functions with an expression body (no statements)",
|
|
12
85
|
category: "Best Practices",
|
|
13
86
|
recommended: true,
|
|
14
87
|
},
|
|
@@ -16,81 +89,79 @@ module.exports = {
|
|
|
16
89
|
schema: [],
|
|
17
90
|
messages: {
|
|
18
91
|
noReturnInView:
|
|
19
|
-
"View components
|
|
92
|
+
"View components must use an expression body: () => (...) instead of () => { return (...) }. Hoist any definitions outside of the arrow function body or into the corresponding Container.",
|
|
93
|
+
mustBeArrowFunction:
|
|
94
|
+
"View components must be an arrow function with an expression body: const {{name}} = (props) => (...). A function declaration or function expression cannot have one, so it always carries a statement list — move that logic into the corresponding Container.",
|
|
20
95
|
},
|
|
21
96
|
},
|
|
22
97
|
|
|
23
98
|
create(context) {
|
|
24
|
-
|
|
25
|
-
const normalizedPath = filename.replace(/\\/g, "/");
|
|
26
|
-
|
|
27
|
-
// Only check View.tsx and View.jsx files
|
|
28
|
-
if (!filename.endsWith("View.tsx") && !filename.endsWith("View.jsx")) {
|
|
99
|
+
if (!isEnforcedViewFile(context.getFilename())) {
|
|
29
100
|
return {};
|
|
30
101
|
}
|
|
31
102
|
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
103
|
+
/** Statement-bodied functions that are not arrows, pending a verdict. */
|
|
104
|
+
const nonArrowCandidates = [];
|
|
105
|
+
/** Names the default export resolves to. */
|
|
106
|
+
const exportedNames = new Set();
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Record a non-arrow function that may be this file's View component.
|
|
110
|
+
* @param {object} node - The FunctionDeclaration or FunctionExpression.
|
|
111
|
+
* @param {string|null} name - Its declared name, when it has one.
|
|
112
|
+
* @returns {void}
|
|
113
|
+
*/
|
|
114
|
+
const considerNonArrow = (node, name) => {
|
|
115
|
+
const isDefaultExported =
|
|
116
|
+
node.parent && node.parent.type === "ExportDefaultDeclaration";
|
|
117
|
+
nonArrowCandidates.push({ node, name, isDefaultExported });
|
|
118
|
+
};
|
|
47
119
|
|
|
48
120
|
return {
|
|
49
121
|
ArrowFunctionExpression(node) {
|
|
50
|
-
// Check if this is a component (starts with capital letter or is exported)
|
|
51
122
|
const parent = node.parent;
|
|
123
|
+
const name = declaredName(node);
|
|
124
|
+
const isComponent =
|
|
125
|
+
(parent && parent.type === "ExportDefaultDeclaration") ||
|
|
126
|
+
(name !== null && isViewComponentName(name));
|
|
52
127
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
/^[A-Z]/.test(name) && name.includes("View");
|
|
56
|
-
|
|
57
|
-
// Determine if this is a component
|
|
58
|
-
const isComponent = (() => {
|
|
59
|
-
// Check if it's a default export
|
|
60
|
-
if (parent.type === "ExportDefaultDeclaration") {
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Check if it's a variable declaration with PascalCase name
|
|
65
|
-
if (
|
|
66
|
-
parent.type === "VariableDeclarator" &&
|
|
67
|
-
parent.id.type === "Identifier"
|
|
68
|
-
) {
|
|
69
|
-
return isViewComponent(parent.id.name);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Check if it's part of an export statement
|
|
73
|
-
if (
|
|
74
|
-
parent.type === "VariableDeclarator" &&
|
|
75
|
-
parent.parent.type === "VariableDeclaration" &&
|
|
76
|
-
parent.parent.parent.type === "ExportNamedDeclaration"
|
|
77
|
-
) {
|
|
78
|
-
return isViewComponent(parent.id.name);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return false;
|
|
82
|
-
})();
|
|
83
|
-
|
|
84
|
-
if (!isComponent) return;
|
|
85
|
-
|
|
86
|
-
// Check if the arrow function has a block body (any BlockStatement is forbidden)
|
|
87
|
-
if (node.body.type === "BlockStatement") {
|
|
88
|
-
context.report({
|
|
89
|
-
node,
|
|
90
|
-
messageId: "noReturnInView",
|
|
91
|
-
});
|
|
128
|
+
if (isComponent && node.body.type === "BlockStatement") {
|
|
129
|
+
context.report({ node, messageId: "noReturnInView" });
|
|
92
130
|
}
|
|
93
131
|
},
|
|
132
|
+
|
|
133
|
+
FunctionDeclaration(node) {
|
|
134
|
+
considerNonArrow(node, node.id ? node.id.name : null);
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
FunctionExpression(node) {
|
|
138
|
+
considerNonArrow(node, declaredName(node));
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
ExportDefaultDeclaration(node) {
|
|
142
|
+
const name = defaultExportedName(node.declaration);
|
|
143
|
+
if (name !== null) {
|
|
144
|
+
exportedNames.add(name);
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
"Program:exit"() {
|
|
149
|
+
nonArrowCandidates
|
|
150
|
+
.filter(
|
|
151
|
+
candidate =>
|
|
152
|
+
candidate.isDefaultExported ||
|
|
153
|
+
(candidate.name !== null &&
|
|
154
|
+
(isViewComponentName(candidate.name) ||
|
|
155
|
+
exportedNames.has(candidate.name)))
|
|
156
|
+
)
|
|
157
|
+
.forEach(candidate => {
|
|
158
|
+
context.report({
|
|
159
|
+
node: candidate.node,
|
|
160
|
+
messageId: "mustBeArrowFunction",
|
|
161
|
+
data: { name: candidate.name ?? "ComponentView" },
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
},
|
|
94
165
|
};
|
|
95
166
|
},
|
|
96
167
|
};
|
|
@@ -59,6 +59,39 @@ const componentStructure = require("@codyswann/eslint-plugin-component-structure
|
|
|
59
59
|
const uiStandards = require("@codyswann/eslint-plugin-ui-standards");
|
|
60
60
|
const expoConfig = require("eslint-config-expo/flat");
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* React's hook named exports, banned from a View's import list.
|
|
64
|
+
*
|
|
65
|
+
* An enumeration is correct HERE and nowhere else: React's hook set is fixed and
|
|
66
|
+
* published, and this is the import-line axis. The call-site axis
|
|
67
|
+
* (`component-structure/no-hooks-in-view`) matches by shape precisely because a
|
|
68
|
+
* project-local custom hook can never appear on a list like this one.
|
|
69
|
+
*/
|
|
70
|
+
const REACT_HOOK_IMPORT_NAMES = [
|
|
71
|
+
"useActionState",
|
|
72
|
+
"useCallback",
|
|
73
|
+
"useContext",
|
|
74
|
+
"useDebugValue",
|
|
75
|
+
"useDeferredValue",
|
|
76
|
+
"useEffect",
|
|
77
|
+
"useEffectEvent",
|
|
78
|
+
"useId",
|
|
79
|
+
"useImperativeHandle",
|
|
80
|
+
"useInsertionEffect",
|
|
81
|
+
"useLayoutEffect",
|
|
82
|
+
"useMemo",
|
|
83
|
+
"useOptimistic",
|
|
84
|
+
"useReducer",
|
|
85
|
+
"useRef",
|
|
86
|
+
"useState",
|
|
87
|
+
"useSyncExternalStore",
|
|
88
|
+
"useTransition",
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
/** What a developer is told when a View imports a hook. */
|
|
92
|
+
const VIEW_HOOK_IMPORT_MESSAGE =
|
|
93
|
+
"View components must not use hooks. Move the hook into the corresponding Container and pass its result down as a prop.";
|
|
94
|
+
|
|
62
95
|
/**
|
|
63
96
|
* A project's `.lisa.config.json`, when it has one.
|
|
64
97
|
*
|
|
@@ -371,7 +404,13 @@ export function getExpoConfig({
|
|
|
371
404
|
},
|
|
372
405
|
},
|
|
373
406
|
|
|
374
|
-
// View component rules
|
|
407
|
+
// View component rules: no statements, no hooks.
|
|
408
|
+
//
|
|
409
|
+
// Stated as those two properties rather than as "Views are pure functions",
|
|
410
|
+
// which is a guarantee neither rule delivers — `{Date.now()}` and
|
|
411
|
+
// `{Math.random() > 0.5 ? … : …}` sit happily in an expression body. The
|
|
412
|
+
// skill claiming an enforcement it did not have is what let a hook live in
|
|
413
|
+
// a View for so long; the fix must not repeat the shape of the defect.
|
|
375
414
|
{
|
|
376
415
|
files: ["**/*View.tsx", "**/*View.jsx"],
|
|
377
416
|
rules: {
|
|
@@ -379,6 +418,54 @@ export function getExpoConfig({
|
|
|
379
418
|
},
|
|
380
419
|
},
|
|
381
420
|
|
|
421
|
+
// View hook ban, on two independent axes.
|
|
422
|
+
//
|
|
423
|
+
// `no-hooks-in-view` matches the CALL by shape (`use` + uppercase), so a
|
|
424
|
+
// project-local custom hook is caught where an enumerated name list would
|
|
425
|
+
// miss it. `no-restricted-imports` catches the IMPORT LINE, before a call
|
|
426
|
+
// site exists. Neither subsumes the other: a hook re-exported through a
|
|
427
|
+
// barrel escapes the import patterns, and an import with no call yet
|
|
428
|
+
// escapes the call matcher.
|
|
429
|
+
//
|
|
430
|
+
// `paths` may enumerate here because React's hook set is fixed and
|
|
431
|
+
// published; the generic half is the rule, not this list. Scope matches the
|
|
432
|
+
// Container/View pattern's own applicability table, and the base
|
|
433
|
+
// `@/features/*/*` restriction is carried forward because a flat-config
|
|
434
|
+
// override replaces a rule's options wholesale.
|
|
435
|
+
{
|
|
436
|
+
files: ["**/*View.tsx", "**/*View.jsx"],
|
|
437
|
+
ignores: [
|
|
438
|
+
"components/ui/**",
|
|
439
|
+
"components/shared/**",
|
|
440
|
+
"components/icons/**",
|
|
441
|
+
],
|
|
442
|
+
rules: {
|
|
443
|
+
"component-structure/no-hooks-in-view": "error",
|
|
444
|
+
"no-restricted-imports": [
|
|
445
|
+
"error",
|
|
446
|
+
{
|
|
447
|
+
paths: [
|
|
448
|
+
{
|
|
449
|
+
name: "react",
|
|
450
|
+
importNames: REACT_HOOK_IMPORT_NAMES,
|
|
451
|
+
message: VIEW_HOOK_IMPORT_MESSAGE,
|
|
452
|
+
},
|
|
453
|
+
],
|
|
454
|
+
patterns: [
|
|
455
|
+
// Objects, not a mix: `no-restricted-imports` accepts an array of
|
|
456
|
+
// strings OR an array of objects, never both, and a mixed array
|
|
457
|
+
// is a schema error that disables the whole override.
|
|
458
|
+
{ group: ["@/features/*/*"] },
|
|
459
|
+
{
|
|
460
|
+
group: ["**/hooks/**", "@/hooks/**", "~/hooks/**"],
|
|
461
|
+
message: VIEW_HOOK_IMPORT_MESSAGE,
|
|
462
|
+
},
|
|
463
|
+
],
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
|
|
382
469
|
// View memo requirement (excluding UI components)
|
|
383
470
|
{
|
|
384
471
|
files: ["**/*View.tsx", "**/*View.jsx"],
|
package/package.json
CHANGED
|
@@ -165,7 +165,7 @@
|
|
|
165
165
|
"zod-validation-error": "^4.0.0"
|
|
166
166
|
},
|
|
167
167
|
"name": "@codyswann/lisa",
|
|
168
|
-
"version": "4.33.
|
|
168
|
+
"version": "4.33.5",
|
|
169
169
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
170
170
|
"main": "dist/index.js",
|
|
171
171
|
"exports": {
|
|
@@ -313,7 +313,7 @@
|
|
|
313
313
|
"test": "tests"
|
|
314
314
|
},
|
|
315
315
|
"types": "./dist/index.d.ts",
|
|
316
|
-
"lisaReleaseCommit": "
|
|
317
|
-
"gitHead": "
|
|
318
|
-
"lisaReleaseTag": "v4.33.
|
|
316
|
+
"lisaReleaseCommit": "093c20ef2f67eadda94e473ffe5ff52cd14e1107",
|
|
317
|
+
"gitHead": "093c20ef2f67eadda94e473ffe5ff52cd14e1107",
|
|
318
|
+
"lisaReleaseTag": "v4.33.5"
|
|
319
319
|
}
|