@almadar/eslint-plugin 2.2.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 +55 -3
- package/package.json +7 -1
- package/rules/atom-molecule-no-entity-prop.js +82 -0
- package/rules/no-entity-string-type.js +84 -0
- package/rules/organism-require-entity-row.js +179 -0
- package/rules/require-closed-circuit-props.js +1 -1
- package/rules/require-translate.js +1 -1
- package/rules/service-barrel-complete.js +129 -0
- package/rules/service-external-declared.js +172 -0
- package/rules/service-no-inline-queries.js +117 -0
- package/rules/service-no-workspace-refs.js +76 -0
- package/rules/service-subpath-match-tsup.js +89 -0
- package/rules/service-use-eventbus.js +144 -0
package/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const templateNoIteration = require("./rules/template-no-iteration");
|
|
|
8
8
|
const templateSerializableProps = require("./rules/template-serializable-props");
|
|
9
9
|
const noTypeProp = require("./rules/no-type-prop");
|
|
10
10
|
const noAsAny = require("./rules/no-as-any");
|
|
11
|
+
const noEntityStringType = require("./rules/no-entity-string-type");
|
|
11
12
|
const noImportGenerated = require("./rules/no-import-generated");
|
|
12
13
|
const noUseNavigate = require("./rules/no-use-navigate");
|
|
13
14
|
const requireEventBus = require("./rules/require-event-bus");
|
|
@@ -22,8 +23,12 @@ const organismNoAutoFetch = require("./rules/organism-no-auto-fetch");
|
|
|
22
23
|
const organismNoCallbackProps = require("./rules/organism-no-callback-props");
|
|
23
24
|
const organismExtendsEntityDisplay = require("./rules/organism-extends-entity-display");
|
|
24
25
|
const organismRenderingStateOnly = require("./rules/organism-rendering-state-only");
|
|
26
|
+
const organismRequireEntityRow = require("./rules/organism-require-entity-row");
|
|
25
27
|
|
|
26
|
-
//
|
|
28
|
+
// Atom/molecule rules
|
|
29
|
+
const atomMoleculeNoEntityProp = require("./rules/atom-molecule-no-entity-prop");
|
|
30
|
+
|
|
31
|
+
// Service rules - Phase 1-3 (Implemented)
|
|
27
32
|
const serviceLazySingleton = require("./rules/service-lazy-singleton");
|
|
28
33
|
const serviceSingletonResettable = require("./rules/service-singleton-resettable");
|
|
29
34
|
const serviceNoModuleSideEffects = require("./rules/service-no-module-side-effects");
|
|
@@ -32,6 +37,16 @@ const serviceEventNameScreaming = require("./rules/service-event-name-screaming"
|
|
|
32
37
|
const serviceIntegrationExtendsBase = require("./rules/service-integration-extends-base");
|
|
33
38
|
const serviceStoreIsolation = require("./rules/service-store-isolation");
|
|
34
39
|
|
|
40
|
+
// Service rules - Phase 4 (New)
|
|
41
|
+
const serviceBarrelComplete = require("./rules/service-barrel-complete");
|
|
42
|
+
const serviceSubpathMatchTsup = require("./rules/service-subpath-match-tsup");
|
|
43
|
+
const serviceNoWorkspaceRefs = require("./rules/service-no-workspace-refs");
|
|
44
|
+
const serviceExternalDeclared = require("./rules/service-external-declared");
|
|
45
|
+
|
|
46
|
+
// Service rules - Additional (New)
|
|
47
|
+
const serviceUseEventbus = require("./rules/service-use-eventbus");
|
|
48
|
+
const serviceNoInlineQueries = require("./rules/service-no-inline-queries");
|
|
49
|
+
|
|
35
50
|
const plugin = {
|
|
36
51
|
meta: {
|
|
37
52
|
name: "@almadar/eslint-plugin",
|
|
@@ -47,6 +62,7 @@ const plugin = {
|
|
|
47
62
|
"template-serializable-props": templateSerializableProps,
|
|
48
63
|
"no-type-prop": noTypeProp,
|
|
49
64
|
"no-as-any": noAsAny,
|
|
65
|
+
"no-entity-string-type": noEntityStringType,
|
|
50
66
|
"no-import-generated": noImportGenerated,
|
|
51
67
|
"no-use-navigate": noUseNavigate,
|
|
52
68
|
"require-event-bus": requireEventBus,
|
|
@@ -61,8 +77,12 @@ const plugin = {
|
|
|
61
77
|
"organism-no-callback-props": organismNoCallbackProps,
|
|
62
78
|
"organism-extends-entity-display": organismExtendsEntityDisplay,
|
|
63
79
|
"organism-rendering-state-only": organismRenderingStateOnly,
|
|
80
|
+
"organism-require-entity-row": organismRequireEntityRow,
|
|
64
81
|
|
|
65
|
-
//
|
|
82
|
+
// Atom/molecule rules
|
|
83
|
+
"atom-molecule-no-entity-prop": atomMoleculeNoEntityProp,
|
|
84
|
+
|
|
85
|
+
// Service rules - Phase 1-3
|
|
66
86
|
"service-lazy-singleton": serviceLazySingleton,
|
|
67
87
|
"service-singleton-resettable": serviceSingletonResettable,
|
|
68
88
|
"service-no-module-side-effects": serviceNoModuleSideEffects,
|
|
@@ -70,6 +90,16 @@ const plugin = {
|
|
|
70
90
|
"service-event-name-screaming": serviceEventNameScreaming,
|
|
71
91
|
"service-integration-extends-base": serviceIntegrationExtendsBase,
|
|
72
92
|
"service-store-isolation": serviceStoreIsolation,
|
|
93
|
+
|
|
94
|
+
// Service rules - Phase 4
|
|
95
|
+
"service-barrel-complete": serviceBarrelComplete,
|
|
96
|
+
"service-subpath-match-tsup": serviceSubpathMatchTsup,
|
|
97
|
+
"service-no-workspace-refs": serviceNoWorkspaceRefs,
|
|
98
|
+
"service-external-declared": serviceExternalDeclared,
|
|
99
|
+
|
|
100
|
+
// Service rules - Additional
|
|
101
|
+
"service-use-eventbus": serviceUseEventbus,
|
|
102
|
+
"service-no-inline-queries": serviceNoInlineQueries,
|
|
73
103
|
},
|
|
74
104
|
|
|
75
105
|
configs: {},
|
|
@@ -90,8 +120,10 @@ plugin.configs.recommended = {
|
|
|
90
120
|
"almadar/template-serializable-props": "error",
|
|
91
121
|
"almadar/no-type-prop": "error",
|
|
92
122
|
"almadar/no-as-any": "error",
|
|
123
|
+
"almadar/no-entity-string-type": "error",
|
|
93
124
|
"almadar/no-import-generated": "error",
|
|
94
125
|
"almadar/no-use-navigate": "error",
|
|
126
|
+
"almadar/atom-molecule-no-entity-prop": "error",
|
|
95
127
|
|
|
96
128
|
// Warn-level — conventions that should be followed
|
|
97
129
|
"almadar/require-event-bus": "warn",
|
|
@@ -108,6 +140,7 @@ plugin.configs.recommended = {
|
|
|
108
140
|
"almadar/organism-no-callback-props": "error",
|
|
109
141
|
"almadar/organism-extends-entity-display": "error",
|
|
110
142
|
"almadar/organism-rendering-state-only": "warn",
|
|
143
|
+
"almadar/organism-require-entity-row": "error",
|
|
111
144
|
},
|
|
112
145
|
};
|
|
113
146
|
|
|
@@ -147,6 +180,7 @@ plugin.configs.organisms = {
|
|
|
147
180
|
"almadar/organism-rendering-state-only": "warn",
|
|
148
181
|
"almadar/require-event-bus": "error",
|
|
149
182
|
"almadar/require-closed-circuit-props": "error",
|
|
183
|
+
"almadar/organism-require-entity-row": "error",
|
|
150
184
|
},
|
|
151
185
|
};
|
|
152
186
|
|
|
@@ -154,6 +188,7 @@ plugin.configs.organisms = {
|
|
|
154
188
|
plugin.configs.services = {
|
|
155
189
|
plugins: { almadar: plugin },
|
|
156
190
|
rules: {
|
|
191
|
+
// Phase 1-3
|
|
157
192
|
"almadar/service-lazy-singleton": "error",
|
|
158
193
|
"almadar/service-singleton-resettable": "error",
|
|
159
194
|
"almadar/service-no-module-side-effects": "error",
|
|
@@ -161,13 +196,22 @@ plugin.configs.services = {
|
|
|
161
196
|
"almadar/service-event-name-screaming": "error",
|
|
162
197
|
"almadar/service-integration-extends-base": "error",
|
|
163
198
|
"almadar/service-store-isolation": "error",
|
|
199
|
+
// Phase 4
|
|
200
|
+
"almadar/service-barrel-complete": "warn",
|
|
201
|
+
"almadar/service-subpath-match-tsup": "warn",
|
|
202
|
+
"almadar/service-no-workspace-refs": "error",
|
|
203
|
+
"almadar/service-external-declared": "error",
|
|
204
|
+
// Additional
|
|
205
|
+
"almadar/service-use-eventbus": "warn",
|
|
206
|
+
"almadar/service-no-inline-queries": "error",
|
|
164
207
|
},
|
|
165
208
|
};
|
|
166
209
|
|
|
167
|
-
// Services strict config — all service rules as error
|
|
210
|
+
// Services strict config — all service rules as error
|
|
168
211
|
plugin.configs["services-strict"] = {
|
|
169
212
|
plugins: { almadar: plugin },
|
|
170
213
|
rules: {
|
|
214
|
+
// Phase 1-3
|
|
171
215
|
"almadar/service-lazy-singleton": "error",
|
|
172
216
|
"almadar/service-singleton-resettable": "error",
|
|
173
217
|
"almadar/service-no-module-side-effects": "error",
|
|
@@ -175,6 +219,14 @@ plugin.configs["services-strict"] = {
|
|
|
175
219
|
"almadar/service-event-name-screaming": "error",
|
|
176
220
|
"almadar/service-integration-extends-base": "error",
|
|
177
221
|
"almadar/service-store-isolation": "error",
|
|
222
|
+
// Phase 4
|
|
223
|
+
"almadar/service-barrel-complete": "error",
|
|
224
|
+
"almadar/service-subpath-match-tsup": "error",
|
|
225
|
+
"almadar/service-no-workspace-refs": "error",
|
|
226
|
+
"almadar/service-external-declared": "error",
|
|
227
|
+
// Additional
|
|
228
|
+
"almadar/service-use-eventbus": "error",
|
|
229
|
+
"almadar/service-no-inline-queries": "error",
|
|
178
230
|
},
|
|
179
231
|
};
|
|
180
232
|
|
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,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Rule: no-entity-string-type
|
|
5
|
+
*
|
|
6
|
+
* Prevents typing the `entity` prop as `string` in TypeScript interfaces and type aliases.
|
|
7
|
+
* Entity props must carry actual data (objects, arrays, or domain types), never string identifiers.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
meta: {
|
|
12
|
+
type: "problem",
|
|
13
|
+
docs: {
|
|
14
|
+
description:
|
|
15
|
+
"Disallow typing the 'entity' prop as 'string' — entity carries data, not a type name",
|
|
16
|
+
category: "Almadar Architecture",
|
|
17
|
+
},
|
|
18
|
+
messages: {
|
|
19
|
+
noEntityString:
|
|
20
|
+
"The 'entity' prop must not be typed as 'string'. Use a proper entity type (object, array, or domain interface) instead.",
|
|
21
|
+
},
|
|
22
|
+
schema: [],
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
create(context) {
|
|
26
|
+
/**
|
|
27
|
+
* Check if a type annotation contains 'string' at any level
|
|
28
|
+
* (direct TSStringKeyword, or inside a union/intersection).
|
|
29
|
+
*/
|
|
30
|
+
function typeIncludesString(typeNode) {
|
|
31
|
+
if (!typeNode) return false;
|
|
32
|
+
|
|
33
|
+
// Direct string keyword
|
|
34
|
+
if (typeNode.type === "TSStringKeyword") return true;
|
|
35
|
+
|
|
36
|
+
// Union: string | Foo
|
|
37
|
+
if (typeNode.type === "TSUnionType") {
|
|
38
|
+
return typeNode.types.some(typeIncludesString);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Intersection: string & Foo (unlikely but defensive)
|
|
42
|
+
if (typeNode.type === "TSIntersectionType") {
|
|
43
|
+
return typeNode.types.some(typeIncludesString);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check a property signature (interface member or type literal member)
|
|
51
|
+
*/
|
|
52
|
+
function checkPropertySignature(node) {
|
|
53
|
+
// Only check properties named "entity"
|
|
54
|
+
const key = node.key;
|
|
55
|
+
if (!key) return;
|
|
56
|
+
|
|
57
|
+
const propName =
|
|
58
|
+
key.type === "Identifier"
|
|
59
|
+
? key.name
|
|
60
|
+
: key.type === "Literal"
|
|
61
|
+
? String(key.value)
|
|
62
|
+
: null;
|
|
63
|
+
|
|
64
|
+
if (propName !== "entity") return;
|
|
65
|
+
|
|
66
|
+
// Check the type annotation
|
|
67
|
+
const typeAnnotation = node.typeAnnotation;
|
|
68
|
+
if (!typeAnnotation) return;
|
|
69
|
+
|
|
70
|
+
const typeNode =
|
|
71
|
+
typeAnnotation.type === "TSTypeAnnotation"
|
|
72
|
+
? typeAnnotation.typeAnnotation
|
|
73
|
+
: typeAnnotation;
|
|
74
|
+
|
|
75
|
+
if (typeIncludesString(typeNode)) {
|
|
76
|
+
context.report({ node, messageId: "noEntityString" });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
TSPropertySignature: checkPropertySignature,
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const { isOrganism } = require("../utils/helpers");
|
|
4
4
|
|
|
5
5
|
const REQUIRED_PROPS = ["className"];
|
|
6
|
-
const RECOMMENDED_PROPS = ["isLoading", "error", "entity", "
|
|
6
|
+
const RECOMMENDED_PROPS = ["isLoading", "error", "entity", "sortBy", "sortDirection", "page", "pageSize", "totalCount", "searchValue"];
|
|
7
7
|
|
|
8
8
|
module.exports = {
|
|
9
9
|
meta: {
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Rule to ensure all public functions are exported from barrel files (index.ts)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
meta: {
|
|
12
|
+
type: "problem",
|
|
13
|
+
docs: {
|
|
14
|
+
description: "Ensure all public functions are exported from barrel files (index.ts)",
|
|
15
|
+
category: "Almadar Services",
|
|
16
|
+
recommended: true,
|
|
17
|
+
},
|
|
18
|
+
messages: {
|
|
19
|
+
missingFromBarrel: "Public export '{{name}}' from '{{source}}' is not re-exported in '{{barrel}}'.",
|
|
20
|
+
notExported: "Function '{{name}}' is declared but not exported. Add export keyword or include in index.ts.",
|
|
21
|
+
},
|
|
22
|
+
schema: [
|
|
23
|
+
{
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
barrelFile: {
|
|
27
|
+
type: "string",
|
|
28
|
+
default: "index.ts",
|
|
29
|
+
},
|
|
30
|
+
ignorePrivate: {
|
|
31
|
+
type: "boolean",
|
|
32
|
+
default: true,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
additionalProperties: false,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
create(context) {
|
|
41
|
+
const options = context.options[0] || {};
|
|
42
|
+
const barrelFileName = options.barrelFile || "index.ts";
|
|
43
|
+
void options.ignorePrivate;
|
|
44
|
+
|
|
45
|
+
const filename = context.filename;
|
|
46
|
+
const dir = path.dirname(filename);
|
|
47
|
+
const barrelPath = path.join(dir, barrelFileName);
|
|
48
|
+
|
|
49
|
+
// Skip if this IS the barrel file itself
|
|
50
|
+
if (path.basename(filename) === barrelFileName) {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Skip if no barrel file exists
|
|
55
|
+
if (!fs.existsSync(barrelPath)) {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Read barrel file content
|
|
60
|
+
let barrelContent = "";
|
|
61
|
+
try {
|
|
62
|
+
barrelContent = fs.readFileSync(barrelPath, "utf-8");
|
|
63
|
+
} catch {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Track exports in this file
|
|
68
|
+
const fileExports = new Set();
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
// Export declarations: export function foo() {}
|
|
72
|
+
ExportNamedDeclaration(node) {
|
|
73
|
+
if (node.declaration) {
|
|
74
|
+
if (node.declaration.type === "FunctionDeclaration" && node.declaration.id) {
|
|
75
|
+
fileExports.add(node.declaration.id.name);
|
|
76
|
+
} else if (node.declaration.type === "VariableDeclaration") {
|
|
77
|
+
node.declaration.declarations.forEach((decl) => {
|
|
78
|
+
if (decl.id && decl.id.type === "Identifier") {
|
|
79
|
+
fileExports.add(decl.id.name);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
} else if (node.declaration.type === "ClassDeclaration" && node.declaration.id) {
|
|
83
|
+
fileExports.add(node.declaration.id.name);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Handle: export { foo, bar }
|
|
87
|
+
if (node.specifiers) {
|
|
88
|
+
node.specifiers.forEach((spec) => {
|
|
89
|
+
if (spec.exported && spec.exported.type === "Identifier") {
|
|
90
|
+
fileExports.add(spec.exported.name);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
// Export default: export default foo
|
|
97
|
+
ExportDefaultDeclaration(node) {
|
|
98
|
+
if (node.declaration && node.declaration.type === "Identifier") {
|
|
99
|
+
fileExports.add(node.declaration.name);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
// Check function declarations at end of file
|
|
104
|
+
"Program:exit"() {
|
|
105
|
+
fileExports.forEach((name) => {
|
|
106
|
+
// Check if this export is in the barrel file
|
|
107
|
+
// Look for: export { name } or export * from './this-file'
|
|
108
|
+
const exportPattern = new RegExp(`export\\s*\\{[^}]*\\b${name}\\b`, "i");
|
|
109
|
+
const starExportPattern = new RegExp(`export\\s*\\*\\s*from\\s*['"]\\.\\/${path.basename(filename, path.extname(filename))}['"]`, "i");
|
|
110
|
+
|
|
111
|
+
const isInBarrel = exportPattern.test(barrelContent) ||
|
|
112
|
+
starExportPattern.test(barrelContent);
|
|
113
|
+
|
|
114
|
+
if (!isInBarrel) {
|
|
115
|
+
context.report({
|
|
116
|
+
node: context.getSourceCode().ast,
|
|
117
|
+
messageId: "missingFromBarrel",
|
|
118
|
+
data: {
|
|
119
|
+
name,
|
|
120
|
+
source: path.basename(filename),
|
|
121
|
+
barrel: barrelFileName,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Rule to ensure all imports are declared in package.json dependencies
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
meta: {
|
|
12
|
+
type: "problem",
|
|
13
|
+
docs: {
|
|
14
|
+
description: "Ensure all imports are declared in package.json dependencies",
|
|
15
|
+
category: "Almadar Services",
|
|
16
|
+
recommended: true,
|
|
17
|
+
},
|
|
18
|
+
messages: {
|
|
19
|
+
undeclaredImport: "Import '{{package}}' is not declared in package.json dependencies. Add to dependencies, devDependencies, or peerDependencies.",
|
|
20
|
+
missingPackageJson: "Could not find package.json to validate imports.",
|
|
21
|
+
},
|
|
22
|
+
schema: [
|
|
23
|
+
{
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
allowBuiltin: {
|
|
27
|
+
type: "boolean",
|
|
28
|
+
default: true,
|
|
29
|
+
},
|
|
30
|
+
allowInternal: {
|
|
31
|
+
type: "boolean",
|
|
32
|
+
default: true,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
additionalProperties: false,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
create(context) {
|
|
41
|
+
const options = context.options[0] || {};
|
|
42
|
+
const allowBuiltin = options.allowBuiltin !== false;
|
|
43
|
+
const allowInternal = options.allowInternal !== false;
|
|
44
|
+
|
|
45
|
+
// Find package.json
|
|
46
|
+
let dir = path.dirname(context.filename);
|
|
47
|
+
let pkgPath = null;
|
|
48
|
+
let pkg = null;
|
|
49
|
+
|
|
50
|
+
while (dir !== path.dirname(dir)) {
|
|
51
|
+
const candidate = path.join(dir, "package.json");
|
|
52
|
+
if (fs.existsSync(candidate)) {
|
|
53
|
+
pkgPath = candidate;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
dir = path.dirname(dir);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!pkgPath) {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
65
|
+
} catch {
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Collect all declared dependencies
|
|
70
|
+
const declaredDeps = new Set();
|
|
71
|
+
["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"].forEach((field) => {
|
|
72
|
+
if (pkg[field]) {
|
|
73
|
+
Object.keys(pkg[field]).forEach((dep) => declaredDeps.add(dep));
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Node.js built-in modules
|
|
78
|
+
const builtinModules = new Set([
|
|
79
|
+
"assert", "buffer", "child_process", "cluster", "console", "constants",
|
|
80
|
+
"crypto", "dgram", "dns", "domain", "events", "fs", "http", "https",
|
|
81
|
+
"module", "net", "os", "path", "punycode", "querystring", "readline",
|
|
82
|
+
"repl", "stream", "string_decoder", "sys", "timers", "tls", "tty",
|
|
83
|
+
"url", "util", "v8", "vm", "zlib", "worker_threads", "perf_hooks",
|
|
84
|
+
"async_hooks", "http2", "diagnostics_channel", "stream/web",
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
ImportDeclaration(node) {
|
|
89
|
+
const source = node.source.value;
|
|
90
|
+
|
|
91
|
+
// Skip relative imports
|
|
92
|
+
if (source.startsWith(".") || source.startsWith("/")) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Skip internal package imports (e.g., @almadar/* in monorepo)
|
|
97
|
+
if (allowInternal && source.startsWith("@almadar/")) {
|
|
98
|
+
// Only check if it's actually in the monorepo
|
|
99
|
+
// For now, assume internal packages are allowed
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Extract package name (handle @scope/pkg and scoped packages with subpaths)
|
|
104
|
+
let packageName = source;
|
|
105
|
+
if (source.startsWith("@")) {
|
|
106
|
+
// @scope/pkg or @scope/pkg/subpath
|
|
107
|
+
const parts = source.split("/");
|
|
108
|
+
packageName = `${parts[0]}/${parts[1]}`;
|
|
109
|
+
} else {
|
|
110
|
+
// pkg or pkg/subpath
|
|
111
|
+
packageName = source.split("/")[0];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Skip built-in modules
|
|
115
|
+
if (allowBuiltin && builtinModules.has(packageName)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Skip Node: prefixed imports
|
|
120
|
+
if (packageName.startsWith("node:")) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check if declared
|
|
125
|
+
if (!declaredDeps.has(packageName)) {
|
|
126
|
+
context.report({
|
|
127
|
+
node,
|
|
128
|
+
messageId: "undeclaredImport",
|
|
129
|
+
data: { package: packageName },
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
// Handle require() calls
|
|
135
|
+
CallExpression(node) {
|
|
136
|
+
if (
|
|
137
|
+
node.callee.type === "Identifier" &&
|
|
138
|
+
node.callee.name === "require" &&
|
|
139
|
+
node.arguments.length > 0 &&
|
|
140
|
+
node.arguments[0].type === "Literal" &&
|
|
141
|
+
typeof node.arguments[0].value === "string"
|
|
142
|
+
) {
|
|
143
|
+
const source = node.arguments[0].value;
|
|
144
|
+
|
|
145
|
+
if (source.startsWith(".") || source.startsWith("/")) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (allowInternal && source.startsWith("@almadar/")) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let packageName = source;
|
|
154
|
+
if (source.startsWith("@")) {
|
|
155
|
+
const parts = source.split("/");
|
|
156
|
+
packageName = `${parts[0]}/${parts[1]}`;
|
|
157
|
+
} else {
|
|
158
|
+
packageName = source.split("/")[0];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!declaredDeps.has(packageName)) {
|
|
162
|
+
context.report({
|
|
163
|
+
node,
|
|
164
|
+
messageId: "undeclaredImport",
|
|
165
|
+
data: { package: packageName },
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Rule to prevent inline Firestore queries in route handlers
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: "problem",
|
|
10
|
+
docs: {
|
|
11
|
+
description: "Prevent inline Firestore queries embedded in route handlers",
|
|
12
|
+
category: "Almadar Services",
|
|
13
|
+
recommended: true,
|
|
14
|
+
},
|
|
15
|
+
messages: {
|
|
16
|
+
inlineFirestore: "Inline Firestore query detected. Extract to DataService or use StoreContract abstraction.",
|
|
17
|
+
directDbAccess: "Direct database access in route handler. Use service layer (DataService) instead.",
|
|
18
|
+
firestoreImport: "Firestore imports should not be used directly in route files. Use service abstraction.",
|
|
19
|
+
},
|
|
20
|
+
schema: [
|
|
21
|
+
{
|
|
22
|
+
type: "object",
|
|
23
|
+
properties: {
|
|
24
|
+
routePatterns: {
|
|
25
|
+
type: "array",
|
|
26
|
+
items: { type: "string" },
|
|
27
|
+
default: ["routes/", "src/routes/", "api/"],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
create(context) {
|
|
36
|
+
const options = context.options[0] || {};
|
|
37
|
+
const routePatterns = options.routePatterns || ["routes/", "src/routes/", "api/"];
|
|
38
|
+
|
|
39
|
+
const filename = context.filename;
|
|
40
|
+
|
|
41
|
+
// Check if this is a route file
|
|
42
|
+
const isRouteFile = routePatterns.some((pattern) => filename.includes(pattern));
|
|
43
|
+
|
|
44
|
+
// Also check if it looks like a route handler (contains route handler patterns)
|
|
45
|
+
let hasRouteHandlerPattern = false;
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
// Check for Firestore imports
|
|
49
|
+
ImportDeclaration(node) {
|
|
50
|
+
const source = node.source.value;
|
|
51
|
+
|
|
52
|
+
if (source.includes("firebase-admin") || source.includes("@google-cloud/firestore")) {
|
|
53
|
+
if (isRouteFile) {
|
|
54
|
+
context.report({
|
|
55
|
+
node,
|
|
56
|
+
messageId: "firestoreImport",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// Check for Express route handler patterns
|
|
63
|
+
"CallExpression[callee.object.name='app'][callee.property.name=/^(get|post|put|delete|patch)$/]"(_node) {
|
|
64
|
+
hasRouteHandlerPattern = true;
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
"CallExpression[callee.object.name='router'][callee.property.name=/^(get|post|put|delete|patch)$/]"(_node) {
|
|
68
|
+
hasRouteHandlerPattern = true;
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
// Check for Firestore method calls
|
|
72
|
+
"CallExpression[callee.type='MemberExpression']"(node) {
|
|
73
|
+
const callee = node.callee;
|
|
74
|
+
|
|
75
|
+
// Check for db.collection(), doc.get(), etc.
|
|
76
|
+
if (callee.property && callee.property.type === "Identifier") {
|
|
77
|
+
const methodName = callee.property.name;
|
|
78
|
+
|
|
79
|
+
const firestoreMethods = [
|
|
80
|
+
"collection", "doc", "get", "set", "update", "delete",
|
|
81
|
+
"where", "orderBy", "limit", "startAfter", "endBefore",
|
|
82
|
+
"add", "batch", "runTransaction"
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
if (firestoreMethods.includes(methodName)) {
|
|
86
|
+
// Check if this is being called on a Firestore-like object
|
|
87
|
+
const objectName = callee.object.name ||
|
|
88
|
+
(callee.object.property && callee.object.property.name);
|
|
89
|
+
|
|
90
|
+
const firestoreObjectNames = ["db", "firestore", "admin", "firestoreDb", "firebase"];
|
|
91
|
+
|
|
92
|
+
if (objectName && firestoreObjectNames.some(name =>
|
|
93
|
+
objectName.toLowerCase().includes(name)
|
|
94
|
+
)) {
|
|
95
|
+
if (isRouteFile || hasRouteHandlerPattern) {
|
|
96
|
+
context.report({
|
|
97
|
+
node,
|
|
98
|
+
messageId: "inlineFirestore",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
// Check for direct db access patterns
|
|
107
|
+
"MemberExpression[object.name='db']"(node) {
|
|
108
|
+
if (isRouteFile || hasRouteHandlerPattern) {
|
|
109
|
+
context.report({
|
|
110
|
+
node,
|
|
111
|
+
messageId: "directDbAccess",
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Rule to prevent workspace:* references in published packages
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: "problem",
|
|
10
|
+
docs: {
|
|
11
|
+
description: "Prevent workspace:* references in published package.json dependencies",
|
|
12
|
+
category: "Almadar Services",
|
|
13
|
+
recommended: true,
|
|
14
|
+
},
|
|
15
|
+
messages: {
|
|
16
|
+
workspaceRef: "Workspace reference '{{ref}}' found in '{{field}}'. Use explicit version for published packages.",
|
|
17
|
+
workspaceProtocol: "workspace: protocol found in '{{field}}'. This will fail when package is published.",
|
|
18
|
+
},
|
|
19
|
+
schema: [],
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
create(context) {
|
|
23
|
+
// Only check package.json files
|
|
24
|
+
if (!context.filename.endsWith("package.json")) {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
Program(node) {
|
|
30
|
+
const sourceCode = context.getSourceCode();
|
|
31
|
+
const text = sourceCode.getText();
|
|
32
|
+
|
|
33
|
+
let pkg;
|
|
34
|
+
try {
|
|
35
|
+
pkg = JSON.parse(text);
|
|
36
|
+
} catch {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check if this is a private package
|
|
41
|
+
if (pkg.private === true) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check all dependency fields
|
|
46
|
+
const depFields = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
|
|
47
|
+
|
|
48
|
+
for (const field of depFields) {
|
|
49
|
+
const deps = pkg[field];
|
|
50
|
+
if (!deps || typeof deps !== "object") {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const [, version] of Object.entries(deps)) {
|
|
55
|
+
// Check for workspace:*
|
|
56
|
+
if (version === "workspace:*" || version === "workspace:") {
|
|
57
|
+
context.report({
|
|
58
|
+
node,
|
|
59
|
+
messageId: "workspaceRef",
|
|
60
|
+
data: { ref: version, field },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// Check for any workspace: protocol
|
|
64
|
+
if (typeof version === "string" && version.startsWith("workspace:")) {
|
|
65
|
+
context.report({
|
|
66
|
+
node,
|
|
67
|
+
messageId: "workspaceProtocol",
|
|
68
|
+
data: { field },
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Rule to ensure package.json exports match tsup build configuration
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
meta: {
|
|
12
|
+
type: "problem",
|
|
13
|
+
docs: {
|
|
14
|
+
description: "Ensure package.json exports match tsup build entries",
|
|
15
|
+
category: "Almadar Services",
|
|
16
|
+
recommended: true,
|
|
17
|
+
},
|
|
18
|
+
messages: {
|
|
19
|
+
exportMismatch: "package.json export '{{exportPath}}' does not match any tsup entry.",
|
|
20
|
+
missingExport: "tsup entry '{{entry}}' is not exported in package.json.",
|
|
21
|
+
invalidPattern: "Export pattern '{{pattern}}' does not match expected format (./dist/*.js).",
|
|
22
|
+
},
|
|
23
|
+
schema: [],
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
create(context) {
|
|
27
|
+
// Only check package.json files
|
|
28
|
+
if (!context.filename.endsWith("package.json")) {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const dir = path.dirname(context.filename);
|
|
33
|
+
const tsupConfigPath = path.join(dir, "tsup.config.ts");
|
|
34
|
+
|
|
35
|
+
// Skip if no tsup config
|
|
36
|
+
if (!fs.existsSync(tsupConfigPath)) {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
Program(node) {
|
|
42
|
+
const sourceCode = context.getSourceCode();
|
|
43
|
+
const text = sourceCode.getText();
|
|
44
|
+
|
|
45
|
+
let pkg;
|
|
46
|
+
try {
|
|
47
|
+
pkg = JSON.parse(text);
|
|
48
|
+
} catch {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Read tsup config (simplified - just check if entries exist)
|
|
53
|
+
let tsupContent = "";
|
|
54
|
+
try {
|
|
55
|
+
tsupContent = fs.readFileSync(tsupConfigPath, "utf-8");
|
|
56
|
+
} catch {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Extract entry points from tsup config
|
|
61
|
+
// Look for: entry: ['src/index.ts'] or entry: { main: 'src/index.ts' }
|
|
62
|
+
const entryMatch = tsupContent.match(/entry\s*:\s*(\[[^\]]+\]|\{[^}]+\})/);
|
|
63
|
+
if (!entryMatch) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const exports = pkg.exports || {};
|
|
68
|
+
|
|
69
|
+
// Check each export has a corresponding tsup entry
|
|
70
|
+
for (const [exportPath] of Object.entries(exports)) {
|
|
71
|
+
if (exportPath.startsWith("./")) {
|
|
72
|
+
// Check if there's a matching entry in tsup
|
|
73
|
+
const hasMatchingEntry = tsupContent.includes(exportPath.replace(/^\.\//, "src/")) ||
|
|
74
|
+
tsupContent.includes(exportPath.replace(/^\.\//, "")) ||
|
|
75
|
+
(exportPath === "." && tsupContent.includes("src/index"));
|
|
76
|
+
|
|
77
|
+
if (!hasMatchingEntry) {
|
|
78
|
+
context.report({
|
|
79
|
+
node,
|
|
80
|
+
messageId: "exportMismatch",
|
|
81
|
+
data: { exportPath },
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Rule to enforce EventBus usage over direct function calls between modules
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const path = require("path");
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: "suggestion",
|
|
12
|
+
docs: {
|
|
13
|
+
description: "Enforce EventBus usage over direct function calls between modules",
|
|
14
|
+
category: "Almadar Services",
|
|
15
|
+
recommended: true,
|
|
16
|
+
},
|
|
17
|
+
messages: {
|
|
18
|
+
directCall: "Cross-module function call to '{{function}}' from '{{module}}'. Consider using EventBus for loose coupling.",
|
|
19
|
+
directImport: "Direct import from service module '{{module}}'. Use EventBus or service factory instead.",
|
|
20
|
+
useEventBus: "Use getGlobalEventBus() or eventBus.emit() for cross-module communication instead of direct function calls.",
|
|
21
|
+
},
|
|
22
|
+
schema: [
|
|
23
|
+
{
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
servicePaths: {
|
|
27
|
+
type: "array",
|
|
28
|
+
items: { type: "string" },
|
|
29
|
+
default: ["src/services/", "src/lib/"],
|
|
30
|
+
},
|
|
31
|
+
allowSameDirectory: {
|
|
32
|
+
type: "boolean",
|
|
33
|
+
default: true,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
additionalProperties: false,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
create(context) {
|
|
42
|
+
const options = context.options[0] || {};
|
|
43
|
+
const servicePaths = options.servicePaths || ["src/services/", "src/lib/"];
|
|
44
|
+
const allowSameDirectory = options.allowSameDirectory !== false;
|
|
45
|
+
|
|
46
|
+
const filename = context.filename;
|
|
47
|
+
const currentDir = path.dirname(filename);
|
|
48
|
+
|
|
49
|
+
// Track imports from service directories
|
|
50
|
+
const serviceImports = new Map();
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
ImportDeclaration(node) {
|
|
54
|
+
const source = node.source.value;
|
|
55
|
+
|
|
56
|
+
// Check if importing from a service path
|
|
57
|
+
const isServiceImport = servicePaths.some(sp =>
|
|
58
|
+
source.includes(sp) || source.includes("/services/")
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
if (!isServiceImport) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Allow same directory imports
|
|
66
|
+
if (allowSameDirectory) {
|
|
67
|
+
const importDir = path.dirname(path.resolve(currentDir, source));
|
|
68
|
+
if (importDir === currentDir) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Track the imported bindings
|
|
74
|
+
node.specifiers.forEach((spec) => {
|
|
75
|
+
if (spec.type === "ImportSpecifier" && spec.imported) {
|
|
76
|
+
serviceImports.set(spec.local.name, {
|
|
77
|
+
source,
|
|
78
|
+
imported: spec.imported.name,
|
|
79
|
+
});
|
|
80
|
+
} else if (spec.type === "ImportDefaultSpecifier") {
|
|
81
|
+
serviceImports.set(spec.local.name, {
|
|
82
|
+
source,
|
|
83
|
+
imported: "default",
|
|
84
|
+
});
|
|
85
|
+
} else if (spec.type === "ImportNamespaceSpecifier") {
|
|
86
|
+
serviceImports.set(spec.local.name, {
|
|
87
|
+
source,
|
|
88
|
+
imported: "*",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Report direct service imports
|
|
94
|
+
if (source.includes("/services/") && !source.startsWith(".")) {
|
|
95
|
+
context.report({
|
|
96
|
+
node,
|
|
97
|
+
messageId: "directImport",
|
|
98
|
+
data: { module: source },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
// Check for direct function calls to imported service functions
|
|
104
|
+
CallExpression(node) {
|
|
105
|
+
if (node.callee.type === "Identifier") {
|
|
106
|
+
const name = node.callee.name;
|
|
107
|
+
const importInfo = serviceImports.get(name);
|
|
108
|
+
|
|
109
|
+
if (importInfo && importInfo.source.includes("/services/")) {
|
|
110
|
+
context.report({
|
|
111
|
+
node,
|
|
112
|
+
messageId: "directCall",
|
|
113
|
+
data: {
|
|
114
|
+
function: name,
|
|
115
|
+
module: importInfo.source,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check for member expressions: Service.method()
|
|
122
|
+
if (
|
|
123
|
+
node.callee.type === "MemberExpression" &&
|
|
124
|
+
node.callee.object.type === "Identifier"
|
|
125
|
+
) {
|
|
126
|
+
const objectName = node.callee.object.name;
|
|
127
|
+
const importInfo = serviceImports.get(objectName);
|
|
128
|
+
|
|
129
|
+
if (importInfo && importInfo.source.includes("/services/")) {
|
|
130
|
+
const methodName = node.callee.property.name || node.callee.property.value;
|
|
131
|
+
context.report({
|
|
132
|
+
node,
|
|
133
|
+
messageId: "directCall",
|
|
134
|
+
data: {
|
|
135
|
+
function: `${objectName}.${methodName}`,
|
|
136
|
+
module: importInfo.source,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
};
|