@almadar/eslint-plugin 2.3.0 → 2.4.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/index.js +11 -0
- package/package.json +7 -1
- package/rules/atom-molecule-no-entity-prop.js +82 -0
- package/rules/organism-require-entity-row.js +179 -0
- package/rules/require-translate.js +1 -1
- package/rules/service-barrel-complete.js +2 -2
- package/rules/service-no-inline-queries.js +2 -2
- package/rules/service-no-workspace-refs.js +1 -1
- package/rules/service-subpath-match-tsup.js +1 -5
package/index.js
CHANGED
|
@@ -23,6 +23,10 @@ const organismNoAutoFetch = require("./rules/organism-no-auto-fetch");
|
|
|
23
23
|
const organismNoCallbackProps = require("./rules/organism-no-callback-props");
|
|
24
24
|
const organismExtendsEntityDisplay = require("./rules/organism-extends-entity-display");
|
|
25
25
|
const organismRenderingStateOnly = require("./rules/organism-rendering-state-only");
|
|
26
|
+
const organismRequireEntityRow = require("./rules/organism-require-entity-row");
|
|
27
|
+
|
|
28
|
+
// Atom/molecule rules
|
|
29
|
+
const atomMoleculeNoEntityProp = require("./rules/atom-molecule-no-entity-prop");
|
|
26
30
|
|
|
27
31
|
// Service rules - Phase 1-3 (Implemented)
|
|
28
32
|
const serviceLazySingleton = require("./rules/service-lazy-singleton");
|
|
@@ -73,6 +77,10 @@ const plugin = {
|
|
|
73
77
|
"organism-no-callback-props": organismNoCallbackProps,
|
|
74
78
|
"organism-extends-entity-display": organismExtendsEntityDisplay,
|
|
75
79
|
"organism-rendering-state-only": organismRenderingStateOnly,
|
|
80
|
+
"organism-require-entity-row": organismRequireEntityRow,
|
|
81
|
+
|
|
82
|
+
// Atom/molecule rules
|
|
83
|
+
"atom-molecule-no-entity-prop": atomMoleculeNoEntityProp,
|
|
76
84
|
|
|
77
85
|
// Service rules - Phase 1-3
|
|
78
86
|
"service-lazy-singleton": serviceLazySingleton,
|
|
@@ -115,6 +123,7 @@ plugin.configs.recommended = {
|
|
|
115
123
|
"almadar/no-entity-string-type": "error",
|
|
116
124
|
"almadar/no-import-generated": "error",
|
|
117
125
|
"almadar/no-use-navigate": "error",
|
|
126
|
+
"almadar/atom-molecule-no-entity-prop": "error",
|
|
118
127
|
|
|
119
128
|
// Warn-level — conventions that should be followed
|
|
120
129
|
"almadar/require-event-bus": "warn",
|
|
@@ -131,6 +140,7 @@ plugin.configs.recommended = {
|
|
|
131
140
|
"almadar/organism-no-callback-props": "error",
|
|
132
141
|
"almadar/organism-extends-entity-display": "error",
|
|
133
142
|
"almadar/organism-rendering-state-only": "warn",
|
|
143
|
+
"almadar/organism-require-entity-row": "error",
|
|
134
144
|
},
|
|
135
145
|
};
|
|
136
146
|
|
|
@@ -170,6 +180,7 @@ plugin.configs.organisms = {
|
|
|
170
180
|
"almadar/organism-rendering-state-only": "warn",
|
|
171
181
|
"almadar/require-event-bus": "error",
|
|
172
182
|
"almadar/require-closed-circuit-props": "error",
|
|
183
|
+
"almadar/organism-require-entity-row": "error",
|
|
173
184
|
},
|
|
174
185
|
};
|
|
175
186
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@almadar/eslint-plugin",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Custom ESLint rules for Almadar component architecture",
|
|
6
6
|
"main": "index.js",
|
|
@@ -12,11 +12,17 @@
|
|
|
12
12
|
"peerDependencies": {
|
|
13
13
|
"eslint": ">=8.0.0"
|
|
14
14
|
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"eslint": "10.0.0"
|
|
17
|
+
},
|
|
15
18
|
"repository": {
|
|
16
19
|
"type": "git",
|
|
17
20
|
"url": "https://github.com/almadar-io/almadar-eslint.git"
|
|
18
21
|
},
|
|
19
22
|
"publishConfig": {
|
|
20
23
|
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"lint": "eslint index.js rules/ utils/"
|
|
21
27
|
}
|
|
22
28
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const { isAtomOrMolecule, isStoryFile } = require("../utils/helpers");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Exceptions: molecules that legitimately need entity-like array data props
|
|
6
|
+
* because they are data-iteration components (mini-organisms promoted to molecules).
|
|
7
|
+
*/
|
|
8
|
+
const EXCEPTED_FILES = new Set([
|
|
9
|
+
"DataGrid",
|
|
10
|
+
"DataList",
|
|
11
|
+
"ButtonGroup", // uses entity as string name for event context
|
|
12
|
+
"FilterGroup", // uses entity as string name for filter events
|
|
13
|
+
"SearchInput", // uses entity as string name for search events
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
function isExcepted(filename) {
|
|
17
|
+
for (const name of EXCEPTED_FILES) {
|
|
18
|
+
if (filename.endsWith(`/${name}.tsx`) || filename.endsWith(`/${name}.jsx`)) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
meta: {
|
|
27
|
+
type: "problem",
|
|
28
|
+
docs: {
|
|
29
|
+
description:
|
|
30
|
+
"Disallow 'entity' prop on atoms and molecules. Atoms/molecules are pure UI building blocks that accept normal typed props. Only organisms and templates should accept 'entity'.",
|
|
31
|
+
category: "Almadar Architecture",
|
|
32
|
+
},
|
|
33
|
+
messages: {
|
|
34
|
+
noEntityProp:
|
|
35
|
+
"Atoms and molecules must not accept an 'entity' prop. Use normal typed props instead (e.g., markers, nodes, items). Only organisms and templates should be entity-aware.",
|
|
36
|
+
},
|
|
37
|
+
schema: [],
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
create(context) {
|
|
41
|
+
const filename = context.filename || context.getFilename();
|
|
42
|
+
if (!isAtomOrMolecule(filename)) return {};
|
|
43
|
+
if (isStoryFile(filename)) return {};
|
|
44
|
+
if (isExcepted(filename)) return {};
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
// Check interface declarations: interface FooProps { entity?: ... }
|
|
48
|
+
TSPropertySignature(node) {
|
|
49
|
+
if (
|
|
50
|
+
node.key?.type === "Identifier" &&
|
|
51
|
+
node.key.name === "entity"
|
|
52
|
+
) {
|
|
53
|
+
// Only flag if inside an interface ending with "Props"
|
|
54
|
+
const parent = node.parent?.parent;
|
|
55
|
+
if (
|
|
56
|
+
parent?.type === "TSInterfaceDeclaration" &&
|
|
57
|
+
parent.id?.name?.endsWith("Props")
|
|
58
|
+
) {
|
|
59
|
+
context.report({ node, messageId: "noEntityProp" });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
// Check type alias declarations: type FooProps = { entity?: ... }
|
|
65
|
+
TSTypeAliasDeclaration(node) {
|
|
66
|
+
if (!node.id?.name?.endsWith("Props")) return;
|
|
67
|
+
const typeAnnotation = node.typeAnnotation;
|
|
68
|
+
if (typeAnnotation?.type !== "TSTypeLiteral") return;
|
|
69
|
+
|
|
70
|
+
for (const member of typeAnnotation.members) {
|
|
71
|
+
if (
|
|
72
|
+
member.type === "TSPropertySignature" &&
|
|
73
|
+
member.key?.type === "Identifier" &&
|
|
74
|
+
member.key.name === "entity"
|
|
75
|
+
) {
|
|
76
|
+
context.report({ node: member, messageId: "noEntityProp" });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const { isOrganism, isStoryFile, isGameOrganism } = require("../utils/helpers");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Find the outermost JSX element returned from a .map() callback.
|
|
6
|
+
* Handles: arrow with expression body, arrow with block body + return,
|
|
7
|
+
* and conditional expressions.
|
|
8
|
+
*/
|
|
9
|
+
function getReturnedJSX(callbackBody) {
|
|
10
|
+
if (
|
|
11
|
+
callbackBody.type === "JSXElement" ||
|
|
12
|
+
callbackBody.type === "JSXFragment"
|
|
13
|
+
) {
|
|
14
|
+
return callbackBody;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (callbackBody.type === "ConditionalExpression") {
|
|
18
|
+
return (
|
|
19
|
+
getReturnedJSX(callbackBody.consequent) ||
|
|
20
|
+
getReturnedJSX(callbackBody.alternate)
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (callbackBody.type === "BlockStatement") {
|
|
25
|
+
for (const stmt of callbackBody.body) {
|
|
26
|
+
if (stmt.type === "ReturnStatement" && stmt.argument) {
|
|
27
|
+
const jsx = getReturnedJSX(stmt.argument);
|
|
28
|
+
if (jsx) return jsx;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if a JSX element has a specific attribute (by name).
|
|
38
|
+
*/
|
|
39
|
+
function hasJSXAttribute(jsxElement, attrName) {
|
|
40
|
+
if (jsxElement.type !== "JSXElement") return false;
|
|
41
|
+
return jsxElement.openingElement.attributes.some(
|
|
42
|
+
(attr) =>
|
|
43
|
+
attr.type === "JSXAttribute" &&
|
|
44
|
+
attr.name?.type === "JSXIdentifier" &&
|
|
45
|
+
attr.name.name === attrName
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if a JSX element has key={param.id} where param is the callback's
|
|
51
|
+
* first parameter. This distinguishes entity iteration (key={hero.id})
|
|
52
|
+
* from utility iteration (key={field}, key={index}, key={actionIdx}).
|
|
53
|
+
*/
|
|
54
|
+
function hasEntityKeyPattern(jsxElement, callbackParam) {
|
|
55
|
+
if (jsxElement.type !== "JSXElement") return false;
|
|
56
|
+
if (!callbackParam) return false;
|
|
57
|
+
|
|
58
|
+
// Get the callback parameter name
|
|
59
|
+
const paramName =
|
|
60
|
+
callbackParam.type === "Identifier"
|
|
61
|
+
? callbackParam.name
|
|
62
|
+
: callbackParam.type === "ObjectPattern"
|
|
63
|
+
? null // destructured param, check differently
|
|
64
|
+
: null;
|
|
65
|
+
|
|
66
|
+
for (const attr of jsxElement.openingElement.attributes) {
|
|
67
|
+
if (attr.type !== "JSXAttribute") continue;
|
|
68
|
+
if (attr.name?.type !== "JSXIdentifier" || attr.name.name !== "key") continue;
|
|
69
|
+
|
|
70
|
+
const value = attr.value;
|
|
71
|
+
if (!value || value.type !== "JSXExpressionContainer") continue;
|
|
72
|
+
|
|
73
|
+
const expr = value.expression;
|
|
74
|
+
|
|
75
|
+
// key={param.id} pattern
|
|
76
|
+
if (
|
|
77
|
+
paramName &&
|
|
78
|
+
expr.type === "MemberExpression" &&
|
|
79
|
+
expr.object?.type === "Identifier" &&
|
|
80
|
+
expr.object.name === paramName &&
|
|
81
|
+
expr.property?.type === "Identifier" &&
|
|
82
|
+
expr.property.name === "id"
|
|
83
|
+
) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// key={param?.id} optional chaining
|
|
88
|
+
if (
|
|
89
|
+
paramName &&
|
|
90
|
+
expr.type === "ChainExpression" &&
|
|
91
|
+
expr.expression?.type === "MemberExpression" &&
|
|
92
|
+
expr.expression.object?.type === "Identifier" &&
|
|
93
|
+
expr.expression.object.name === paramName &&
|
|
94
|
+
expr.expression.property?.type === "Identifier" &&
|
|
95
|
+
expr.expression.property.name === "id"
|
|
96
|
+
) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Destructured: key={id} when param is { id, ... }
|
|
101
|
+
if (
|
|
102
|
+
callbackParam.type === "ObjectPattern" &&
|
|
103
|
+
expr.type === "Identifier" &&
|
|
104
|
+
expr.name === "id"
|
|
105
|
+
) {
|
|
106
|
+
// Verify 'id' is actually destructured from the param
|
|
107
|
+
const hasId = callbackParam.properties.some(
|
|
108
|
+
(p) =>
|
|
109
|
+
p.type === "Property" &&
|
|
110
|
+
p.key?.type === "Identifier" &&
|
|
111
|
+
p.key.name === "id"
|
|
112
|
+
);
|
|
113
|
+
if (hasId) return true;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
meta: {
|
|
122
|
+
type: "problem",
|
|
123
|
+
docs: {
|
|
124
|
+
description:
|
|
125
|
+
"Require data-entity-row attribute on the outermost JSX element returned from .map() in organisms when iterating over entities",
|
|
126
|
+
category: "Almadar Architecture",
|
|
127
|
+
},
|
|
128
|
+
messages: {
|
|
129
|
+
missingEntityRow:
|
|
130
|
+
"Entity row element in .map() is missing 'data-entity-row' attribute. Add it to the root DOM element of each entity card so the verification system can detect entity rows.",
|
|
131
|
+
},
|
|
132
|
+
schema: [],
|
|
133
|
+
},
|
|
134
|
+
create(context) {
|
|
135
|
+
const filename = context.filename || context.getFilename();
|
|
136
|
+
|
|
137
|
+
if (!isOrganism(filename) || isStoryFile(filename)) return {};
|
|
138
|
+
if (isGameOrganism(filename)) return {};
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
CallExpression(node) {
|
|
142
|
+
if (
|
|
143
|
+
node.callee.type !== "MemberExpression" ||
|
|
144
|
+
node.callee.property.type !== "Identifier" ||
|
|
145
|
+
node.callee.property.name !== "map"
|
|
146
|
+
) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const callback = node.arguments[0];
|
|
151
|
+
if (!callback) return;
|
|
152
|
+
|
|
153
|
+
let body;
|
|
154
|
+
if (
|
|
155
|
+
callback.type === "ArrowFunctionExpression" ||
|
|
156
|
+
callback.type === "FunctionExpression"
|
|
157
|
+
) {
|
|
158
|
+
body = callback.body;
|
|
159
|
+
} else {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const jsxElement = getReturnedJSX(body);
|
|
164
|
+
if (!jsxElement || jsxElement.type !== "JSXElement") return;
|
|
165
|
+
|
|
166
|
+
// Only flag entity iteration: key={param.id} pattern
|
|
167
|
+
const callbackParam = callback.params[0];
|
|
168
|
+
if (!hasEntityKeyPattern(jsxElement, callbackParam)) return;
|
|
169
|
+
|
|
170
|
+
if (!hasJSXAttribute(jsxElement, "data-entity-row")) {
|
|
171
|
+
context.report({
|
|
172
|
+
node: jsxElement.openingElement,
|
|
173
|
+
messageId: "missingEntityRow",
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
};
|
|
@@ -40,7 +40,7 @@ module.exports = {
|
|
|
40
40
|
create(context) {
|
|
41
41
|
const options = context.options[0] || {};
|
|
42
42
|
const barrelFileName = options.barrelFile || "index.ts";
|
|
43
|
-
|
|
43
|
+
void options.ignorePrivate;
|
|
44
44
|
|
|
45
45
|
const filename = context.filename;
|
|
46
46
|
const dir = path.dirname(filename);
|
|
@@ -60,7 +60,7 @@ module.exports = {
|
|
|
60
60
|
let barrelContent = "";
|
|
61
61
|
try {
|
|
62
62
|
barrelContent = fs.readFileSync(barrelPath, "utf-8");
|
|
63
|
-
} catch
|
|
63
|
+
} catch {
|
|
64
64
|
return {};
|
|
65
65
|
}
|
|
66
66
|
|
|
@@ -60,11 +60,11 @@ module.exports = {
|
|
|
60
60
|
},
|
|
61
61
|
|
|
62
62
|
// Check for Express route handler patterns
|
|
63
|
-
"CallExpression[callee.object.name='app'][callee.property.name=/^(get|post|put|delete|patch)$/]"(
|
|
63
|
+
"CallExpression[callee.object.name='app'][callee.property.name=/^(get|post|put|delete|patch)$/]"(_node) {
|
|
64
64
|
hasRouteHandlerPattern = true;
|
|
65
65
|
},
|
|
66
66
|
|
|
67
|
-
"CallExpression[callee.object.name='router'][callee.property.name=/^(get|post|put|delete|patch)$/]"(
|
|
67
|
+
"CallExpression[callee.object.name='router'][callee.property.name=/^(get|post|put|delete|patch)$/]"(_node) {
|
|
68
68
|
hasRouteHandlerPattern = true;
|
|
69
69
|
},
|
|
70
70
|
|
|
@@ -51,7 +51,7 @@ module.exports = {
|
|
|
51
51
|
continue;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
for (const [
|
|
54
|
+
for (const [, version] of Object.entries(deps)) {
|
|
55
55
|
// Check for workspace:*
|
|
56
56
|
if (version === "workspace:*" || version === "workspace:") {
|
|
57
57
|
context.report({
|
|
@@ -67,12 +67,8 @@ module.exports = {
|
|
|
67
67
|
const exports = pkg.exports || {};
|
|
68
68
|
|
|
69
69
|
// Check each export has a corresponding tsup entry
|
|
70
|
-
for (const [exportPath
|
|
70
|
+
for (const [exportPath] of Object.entries(exports)) {
|
|
71
71
|
if (exportPath.startsWith("./")) {
|
|
72
|
-
const expectedPattern = exportPath === "."
|
|
73
|
-
? "index.js"
|
|
74
|
-
: `${exportPath.replace(/^\.\//, "/")}.js`;
|
|
75
|
-
|
|
76
72
|
// Check if there's a matching entry in tsup
|
|
77
73
|
const hasMatchingEntry = tsupContent.includes(exportPath.replace(/^\.\//, "src/")) ||
|
|
78
74
|
tsupContent.includes(exportPath.replace(/^\.\//, "")) ||
|