@almadar/eslint-plugin 2.3.0 → 2.4.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.
- package/index.js +33 -0
- package/package.json +10 -1
- package/rules/atom-molecule-no-entity-prop.js +82 -0
- package/rules/behavior-no-organisms.js +166 -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,13 @@ 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");
|
|
30
|
+
|
|
31
|
+
// Behavior rules
|
|
32
|
+
const behaviorNoOrganisms = require("./rules/behavior-no-organisms");
|
|
26
33
|
|
|
27
34
|
// Service rules - Phase 1-3 (Implemented)
|
|
28
35
|
const serviceLazySingleton = require("./rules/service-lazy-singleton");
|
|
@@ -73,6 +80,13 @@ const plugin = {
|
|
|
73
80
|
"organism-no-callback-props": organismNoCallbackProps,
|
|
74
81
|
"organism-extends-entity-display": organismExtendsEntityDisplay,
|
|
75
82
|
"organism-rendering-state-only": organismRenderingStateOnly,
|
|
83
|
+
"organism-require-entity-row": organismRequireEntityRow,
|
|
84
|
+
|
|
85
|
+
// Atom/molecule rules
|
|
86
|
+
"atom-molecule-no-entity-prop": atomMoleculeNoEntityProp,
|
|
87
|
+
|
|
88
|
+
// Behavior rules
|
|
89
|
+
"behavior-no-organisms": behaviorNoOrganisms,
|
|
76
90
|
|
|
77
91
|
// Service rules - Phase 1-3
|
|
78
92
|
"service-lazy-singleton": serviceLazySingleton,
|
|
@@ -115,6 +129,7 @@ plugin.configs.recommended = {
|
|
|
115
129
|
"almadar/no-entity-string-type": "error",
|
|
116
130
|
"almadar/no-import-generated": "error",
|
|
117
131
|
"almadar/no-use-navigate": "error",
|
|
132
|
+
"almadar/atom-molecule-no-entity-prop": "error",
|
|
118
133
|
|
|
119
134
|
// Warn-level — conventions that should be followed
|
|
120
135
|
"almadar/require-event-bus": "warn",
|
|
@@ -125,12 +140,16 @@ plugin.configs.recommended = {
|
|
|
125
140
|
"almadar/template-extends-base": "warn",
|
|
126
141
|
"almadar/require-translate": "warn",
|
|
127
142
|
|
|
143
|
+
// Behavior rules — restrict to atoms/molecules only
|
|
144
|
+
"almadar/behavior-no-organisms": "error",
|
|
145
|
+
|
|
128
146
|
// Organism rules — enforce dumb organism pattern
|
|
129
147
|
"almadar/organism-no-data-state": "error",
|
|
130
148
|
"almadar/organism-no-auto-fetch": "error",
|
|
131
149
|
"almadar/organism-no-callback-props": "error",
|
|
132
150
|
"almadar/organism-extends-entity-display": "error",
|
|
133
151
|
"almadar/organism-rendering-state-only": "warn",
|
|
152
|
+
"almadar/organism-require-entity-row": "error",
|
|
134
153
|
},
|
|
135
154
|
};
|
|
136
155
|
|
|
@@ -170,6 +189,7 @@ plugin.configs.organisms = {
|
|
|
170
189
|
"almadar/organism-rendering-state-only": "warn",
|
|
171
190
|
"almadar/require-event-bus": "error",
|
|
172
191
|
"almadar/require-closed-circuit-props": "error",
|
|
192
|
+
"almadar/organism-require-entity-row": "error",
|
|
173
193
|
},
|
|
174
194
|
};
|
|
175
195
|
|
|
@@ -219,4 +239,17 @@ plugin.configs["services-strict"] = {
|
|
|
219
239
|
},
|
|
220
240
|
};
|
|
221
241
|
|
|
242
|
+
// Behaviors config — lint .orb behavior schemas for pattern tier compliance
|
|
243
|
+
// Requires: jsonc-eslint-parser (pnpm add -D jsonc-eslint-parser)
|
|
244
|
+
// Usage in eslint.config.js: { files: ["**/*.orb"], ...almadar.configs.behaviors }
|
|
245
|
+
plugin.configs.behaviors = {
|
|
246
|
+
plugins: { almadar: plugin },
|
|
247
|
+
languageOptions: {
|
|
248
|
+
parser: require("jsonc-eslint-parser"),
|
|
249
|
+
},
|
|
250
|
+
rules: {
|
|
251
|
+
"almadar/behavior-no-organisms": "error",
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
|
|
222
255
|
module.exports = plugin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@almadar/eslint-plugin",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Custom ESLint rules for Almadar component architecture",
|
|
6
6
|
"main": "index.js",
|
|
@@ -12,11 +12,20 @@
|
|
|
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
|
+
"dependencies": {
|
|
26
|
+
"jsonc-eslint-parser": "3.1.0"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"lint": "eslint index.js rules/ utils/"
|
|
21
30
|
}
|
|
22
31
|
}
|
|
@@ -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,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* behavior-no-organisms
|
|
5
|
+
*
|
|
6
|
+
* Behaviors (.orb files in behaviors/) must only use atom and molecule patterns.
|
|
7
|
+
* Organisms and templates are forbidden, with exceptions for:
|
|
8
|
+
* - Game-related patterns (category "game" in component-mapping.json)
|
|
9
|
+
* - form / form-section (explicitly allowed by convention)
|
|
10
|
+
*
|
|
11
|
+
* Requires: jsonc-eslint-parser configured for *.orb files.
|
|
12
|
+
*
|
|
13
|
+
* To regenerate BLOCKED_PATTERNS from component-mapping.json:
|
|
14
|
+
* node -e "
|
|
15
|
+
* const m = require('../almadar-patterns/src/component-mapping.json').mappings;
|
|
16
|
+
* const blocked = Object.entries(m)
|
|
17
|
+
* .filter(([k, v]) =>
|
|
18
|
+
* (v.importPath.includes('/organisms/') || v.importPath.includes('/templates/'))
|
|
19
|
+
* && v.category !== 'game'
|
|
20
|
+
* && !['form', 'form-section'].includes(k))
|
|
21
|
+
* .map(([k]) => k);
|
|
22
|
+
* console.log(JSON.stringify(blocked, null, 2));
|
|
23
|
+
* "
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
// Organism patterns: importPath contains /organisms/, category !== "game"
|
|
27
|
+
// Template patterns: importPath contains /templates/, not game-related
|
|
28
|
+
// Excludes: form, form-section (allowed by convention)
|
|
29
|
+
const BLOCKED_PATTERNS = new Set([
|
|
30
|
+
// ── Organisms (non-game, non-form) ────────────────────────────────
|
|
31
|
+
"page-header",
|
|
32
|
+
"entity-list",
|
|
33
|
+
"entity-table",
|
|
34
|
+
"entity-cards",
|
|
35
|
+
"stats",
|
|
36
|
+
"detail-panel",
|
|
37
|
+
"wizard-container",
|
|
38
|
+
"confirm-dialog",
|
|
39
|
+
"master-detail",
|
|
40
|
+
"dashboard-grid",
|
|
41
|
+
"runtime-debugger",
|
|
42
|
+
"button-pattern",
|
|
43
|
+
"custom-pattern",
|
|
44
|
+
"drawer-slot",
|
|
45
|
+
"header",
|
|
46
|
+
"layout-pattern",
|
|
47
|
+
"modal-slot",
|
|
48
|
+
"navigation",
|
|
49
|
+
"orbital-visualization",
|
|
50
|
+
"section",
|
|
51
|
+
"sidebar",
|
|
52
|
+
"split",
|
|
53
|
+
"table",
|
|
54
|
+
"toast-slot",
|
|
55
|
+
"u-i-slot-renderer",
|
|
56
|
+
"split-pane",
|
|
57
|
+
"tabbed-container",
|
|
58
|
+
"collapsible-section",
|
|
59
|
+
"camera3-d",
|
|
60
|
+
"lighting3-d",
|
|
61
|
+
"scene3-d",
|
|
62
|
+
"canvas3-d-error-boundary",
|
|
63
|
+
"canvas3-d-loading-state",
|
|
64
|
+
"model-loader",
|
|
65
|
+
"physics-object3-d",
|
|
66
|
+
"feature-renderer",
|
|
67
|
+
"feature-renderer3-d",
|
|
68
|
+
"unit-renderer",
|
|
69
|
+
"trait-slot",
|
|
70
|
+
"trait-state-viewer",
|
|
71
|
+
"content-renderer",
|
|
72
|
+
"jazari-state-machine",
|
|
73
|
+
"state-machine-view",
|
|
74
|
+
"book-chapter-view",
|
|
75
|
+
"book-cover-page",
|
|
76
|
+
"book-nav-bar",
|
|
77
|
+
"book-table-of-contents",
|
|
78
|
+
"book-viewer",
|
|
79
|
+
"simulation-controls",
|
|
80
|
+
"simulation-graph",
|
|
81
|
+
"chart",
|
|
82
|
+
"timeline",
|
|
83
|
+
"media-gallery",
|
|
84
|
+
"signature-pad",
|
|
85
|
+
"document-viewer",
|
|
86
|
+
"graph-canvas",
|
|
87
|
+
"code-viewer",
|
|
88
|
+
"event-log",
|
|
89
|
+
"object-rule-panel",
|
|
90
|
+
"rule-editor",
|
|
91
|
+
"action-palette",
|
|
92
|
+
"sequence-bar",
|
|
93
|
+
"code-view",
|
|
94
|
+
"state-architect-board",
|
|
95
|
+
"state-node",
|
|
96
|
+
"transition-arrow",
|
|
97
|
+
"variable-panel",
|
|
98
|
+
|
|
99
|
+
// ── Templates (non-game) ──────────────────────────────────────────
|
|
100
|
+
"auth-layout",
|
|
101
|
+
"counter-template",
|
|
102
|
+
"dashboard-layout",
|
|
103
|
+
"generic-app-template",
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
// Alternative names that map to the same organism
|
|
107
|
+
const ALIASES = {
|
|
108
|
+
form: null, // explicitly allowed
|
|
109
|
+
"form-section": null, // explicitly allowed
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
module.exports = {
|
|
113
|
+
meta: {
|
|
114
|
+
type: "problem",
|
|
115
|
+
docs: {
|
|
116
|
+
description:
|
|
117
|
+
"Behavior schemas must only use atom and molecule patterns. " +
|
|
118
|
+
"Organisms and templates (except games and form-section) are forbidden.",
|
|
119
|
+
category: "Almadar Architecture",
|
|
120
|
+
},
|
|
121
|
+
messages: {
|
|
122
|
+
blockedPattern:
|
|
123
|
+
'Pattern "{{pattern}}" is an organism/template. ' +
|
|
124
|
+
"Behaviors must only use atoms and molecules. " +
|
|
125
|
+
"Replace with a molecule-level equivalent or restructure the schema.",
|
|
126
|
+
},
|
|
127
|
+
schema: [],
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
create(context) {
|
|
131
|
+
const filename = context.filename || context.getFilename();
|
|
132
|
+
|
|
133
|
+
// Only apply to .orb files inside behaviors/ directories
|
|
134
|
+
if (!filename.endsWith(".orb") || !filename.includes("behaviors")) {
|
|
135
|
+
return {};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
// jsonc-eslint-parser provides JSONProperty AST nodes
|
|
140
|
+
JSONProperty(node) {
|
|
141
|
+
// Look for "type": "<pattern-name>" properties
|
|
142
|
+
if (
|
|
143
|
+
node.key &&
|
|
144
|
+
node.key.type === "JSONLiteral" &&
|
|
145
|
+
node.key.value === "type" &&
|
|
146
|
+
node.value &&
|
|
147
|
+
node.value.type === "JSONLiteral" &&
|
|
148
|
+
typeof node.value.value === "string"
|
|
149
|
+
) {
|
|
150
|
+
const pattern = node.value.value;
|
|
151
|
+
|
|
152
|
+
// Skip explicitly allowed aliases
|
|
153
|
+
if (pattern in ALIASES) return;
|
|
154
|
+
|
|
155
|
+
if (BLOCKED_PATTERNS.has(pattern)) {
|
|
156
|
+
context.report({
|
|
157
|
+
node: node.value,
|
|
158
|
+
messageId: "blockedPattern",
|
|
159
|
+
data: { pattern },
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
},
|
|
166
|
+
};
|
|
@@ -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(/^\.\//, "")) ||
|