@almadar/eslint-plugin 2.1.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/LICENSE +21 -0
- package/README.md +25 -0
- package/index.js +178 -0
- package/package.json +22 -0
- package/rules/event-naming-convention.js +66 -0
- package/rules/no-as-any.js +30 -0
- package/rules/no-import-generated.js +57 -0
- package/rules/no-raw-dom-elements.js +88 -0
- package/rules/no-type-prop.js +60 -0
- package/rules/organism-extends-entity-display.js +63 -0
- package/rules/organism-no-auto-fetch.js +47 -0
- package/rules/organism-no-callback-props.js +57 -0
- package/rules/organism-no-data-state.js +50 -0
- package/rules/organism-rendering-state-only.js +47 -0
- package/rules/require-closed-circuit-props.js +70 -0
- package/rules/require-display-name.js +83 -0
- package/rules/require-event-bus.js +64 -0
- package/rules/require-stories.js +58 -0
- package/rules/require-translate.js +106 -0
- package/rules/service-event-name-screaming.js +73 -0
- package/rules/service-eventbus-singleton.js +53 -0
- package/rules/service-integration-extends-base.js +48 -0
- package/rules/service-lazy-singleton.js +86 -0
- package/rules/service-no-module-side-effects.js +81 -0
- package/rules/service-singleton-resettable.js +136 -0
- package/rules/service-store-isolation.js +70 -0
- package/rules/template-extends-base.js +55 -0
- package/rules/template-no-callbacks.js +46 -0
- package/rules/template-no-hooks.js +57 -0
- package/rules/template-no-iteration.js +58 -0
- package/rules/template-no-local-variables.js +112 -0
- package/rules/template-serializable-props.js +77 -0
- package/utils/helpers.js +133 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { isTemplateFile } = require("../utils/helpers");
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Disallow local variable declarations in template function bodies",
|
|
10
|
+
category: "Almadar Templates",
|
|
11
|
+
},
|
|
12
|
+
messages: {
|
|
13
|
+
noLocalVar:
|
|
14
|
+
"Local variable '{{name}}' in a template cannot survive the flattener. Access entity fields directly (entity.{{name}}) or move logic to an organism.",
|
|
15
|
+
},
|
|
16
|
+
schema: [],
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
create(context) {
|
|
20
|
+
const filename = context.filename;
|
|
21
|
+
if (!isTemplateFile(filename)) return {};
|
|
22
|
+
|
|
23
|
+
// Track which function is the template component
|
|
24
|
+
let templateFunctionDepth = 0;
|
|
25
|
+
|
|
26
|
+
function isExportedComponent(node) {
|
|
27
|
+
// function MyTemplate(...) or const MyTemplate = ...
|
|
28
|
+
const parent = node.parent;
|
|
29
|
+
if (!parent) return false;
|
|
30
|
+
|
|
31
|
+
// export function MyTemplate
|
|
32
|
+
if (parent.type === "ExportNamedDeclaration") return true;
|
|
33
|
+
|
|
34
|
+
// export const MyTemplate = ...
|
|
35
|
+
if (
|
|
36
|
+
parent.type === "VariableDeclarator" &&
|
|
37
|
+
parent.parent &&
|
|
38
|
+
parent.parent.parent &&
|
|
39
|
+
parent.parent.parent.type === "ExportNamedDeclaration"
|
|
40
|
+
) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// const MyTemplate = ... (top-level, likely exported elsewhere)
|
|
45
|
+
if (parent.type === "VariableDeclarator") {
|
|
46
|
+
const id = parent.id;
|
|
47
|
+
if (id.type === "Identifier" && /Template$/.test(id.name)) return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
// Track entry into the template component function
|
|
55
|
+
"FunctionDeclaration"(node) {
|
|
56
|
+
if (node.id && /Template$/.test(node.id.name)) {
|
|
57
|
+
templateFunctionDepth++;
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"FunctionDeclaration:exit"(node) {
|
|
61
|
+
if (node.id && /Template$/.test(node.id.name)) {
|
|
62
|
+
templateFunctionDepth--;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"ArrowFunctionExpression"(node) {
|
|
66
|
+
if (isExportedComponent(node)) {
|
|
67
|
+
templateFunctionDepth++;
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"ArrowFunctionExpression:exit"(node) {
|
|
71
|
+
if (isExportedComponent(node)) {
|
|
72
|
+
templateFunctionDepth--;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
VariableDeclaration(node) {
|
|
77
|
+
if (templateFunctionDepth <= 0) return;
|
|
78
|
+
|
|
79
|
+
for (const decl of node.declarations) {
|
|
80
|
+
// Allow: const { entity, className } = props (prop destructuring)
|
|
81
|
+
if (
|
|
82
|
+
decl.id.type === "ObjectPattern" &&
|
|
83
|
+
decl.init &&
|
|
84
|
+
decl.init.type === "Identifier"
|
|
85
|
+
) {
|
|
86
|
+
// Check if all destructured keys are known template props
|
|
87
|
+
const propNames = new Set(["entity", "className", "scale", "animationSpeed", "unitScale"]);
|
|
88
|
+
const keys = decl.id.properties
|
|
89
|
+
.filter((p) => p.type === "Property" && p.key.type === "Identifier")
|
|
90
|
+
.map((p) => p.key.name);
|
|
91
|
+
if (keys.every((k) => propNames.has(k))) continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const name =
|
|
95
|
+
decl.id.type === "Identifier"
|
|
96
|
+
? decl.id.name
|
|
97
|
+
: decl.id.type === "ObjectPattern"
|
|
98
|
+
? "{...}"
|
|
99
|
+
: decl.id.type === "ArrayPattern"
|
|
100
|
+
? "[...]"
|
|
101
|
+
: "variable";
|
|
102
|
+
|
|
103
|
+
context.report({
|
|
104
|
+
node: decl,
|
|
105
|
+
messageId: "noLocalVar",
|
|
106
|
+
data: { name },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { isTemplateFile } = require("../utils/helpers");
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Ensure JSX prop values in templates are serializable (no functions, JSX elements, computed expressions)",
|
|
10
|
+
category: "Almadar Templates",
|
|
11
|
+
},
|
|
12
|
+
messages: {
|
|
13
|
+
noJsxElement:
|
|
14
|
+
"JSX elements as prop values cannot survive the flattener. Use a string prop or entity binding.",
|
|
15
|
+
noTemplateLiteral:
|
|
16
|
+
"Template literals with expressions cannot be serialized. Pre-compute on the entity or use a string prop.",
|
|
17
|
+
noComputed:
|
|
18
|
+
"Computed expressions in props cannot survive the flattener. Pre-compute on the entity (server-side).",
|
|
19
|
+
},
|
|
20
|
+
schema: [],
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
create(context) {
|
|
24
|
+
const filename = context.filename;
|
|
25
|
+
if (!isTemplateFile(filename)) return {};
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
JSXAttribute(node) {
|
|
29
|
+
if (!node.value || node.value.type !== "JSXExpressionContainer") return;
|
|
30
|
+
|
|
31
|
+
const expr = node.value.expression;
|
|
32
|
+
|
|
33
|
+
// JSX element as prop value: icon={<Icon name="star" />}
|
|
34
|
+
if (expr.type === "JSXElement" || expr.type === "JSXFragment") {
|
|
35
|
+
context.report({ node, messageId: "noJsxElement" });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Template literal with expressions: text={`Level ${entity.level}`}
|
|
40
|
+
if (
|
|
41
|
+
expr.type === "TemplateLiteral" &&
|
|
42
|
+
expr.expressions.length > 0
|
|
43
|
+
) {
|
|
44
|
+
context.report({ node, messageId: "noTemplateLiteral" });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Binary/unary/conditional computed expressions
|
|
49
|
+
// Allow: entity.field, entity.field.nested, literal values, arrays of objects
|
|
50
|
+
if (
|
|
51
|
+
expr.type === "BinaryExpression" ||
|
|
52
|
+
expr.type === "UnaryExpression" ||
|
|
53
|
+
expr.type === "UpdateExpression" ||
|
|
54
|
+
expr.type === "AssignmentExpression" ||
|
|
55
|
+
expr.type === "SequenceExpression" ||
|
|
56
|
+
expr.type === "NewExpression"
|
|
57
|
+
) {
|
|
58
|
+
context.report({ node, messageId: "noComputed" });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Conditional expression: only flag if branches contain computed values
|
|
63
|
+
// Allow: entity.active ? "primary" : "secondary" (both branches are literals)
|
|
64
|
+
if (expr.type === "ConditionalExpression") {
|
|
65
|
+
const { consequent, alternate } = expr;
|
|
66
|
+
const isSimple = (n) =>
|
|
67
|
+
n.type === "Literal" ||
|
|
68
|
+
n.type === "MemberExpression" ||
|
|
69
|
+
n.type === "Identifier";
|
|
70
|
+
if (!isSimple(consequent) || !isSimple(alternate)) {
|
|
71
|
+
context.report({ node, messageId: "noComputed" });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
};
|
package/utils/helpers.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Check if a file path is a template file (*Template.tsx).
|
|
5
|
+
*/
|
|
6
|
+
function isTemplateFile(filename) {
|
|
7
|
+
return /Template\.(tsx|jsx)$/.test(filename);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if a file is inside an atoms/ or molecules/ directory
|
|
12
|
+
* (these are allowed to use raw DOM elements).
|
|
13
|
+
*/
|
|
14
|
+
function isAtomOrMolecule(filename) {
|
|
15
|
+
return /\/atoms\//.test(filename) || /\/molecules\//.test(filename);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check if a file is inside an organisms/ directory.
|
|
20
|
+
*/
|
|
21
|
+
function isOrganism(filename) {
|
|
22
|
+
return /\/organisms\//.test(filename);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if a file is a Storybook story.
|
|
27
|
+
*/
|
|
28
|
+
function isStoryFile(filename) {
|
|
29
|
+
return /\.stories\.(tsx|jsx|ts|js)$/.test(filename);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* HTML elements that should be replaced with @almadar/ui components.
|
|
34
|
+
*/
|
|
35
|
+
const BLOCKED_HTML_ELEMENTS = new Set([
|
|
36
|
+
"div", "span", "section", "main", "article", "aside", "nav",
|
|
37
|
+
"header", "footer", "p", "h1", "h2", "h3", "h4", "h5", "h6",
|
|
38
|
+
"ul", "ol", "li", "label", "input", "button", "textarea",
|
|
39
|
+
"select", "table", "tr", "td", "th", "thead", "tbody", "tfoot",
|
|
40
|
+
"form", "img", "a",
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* HTML elements that are always allowed (media, SVG, canvas).
|
|
45
|
+
*/
|
|
46
|
+
const ALLOWED_HTML_ELEMENTS = new Set([
|
|
47
|
+
"svg", "path", "circle", "rect", "g", "line", "polygon", "polyline",
|
|
48
|
+
"text", "tspan", "defs", "clipPath", "mask", "use", "symbol",
|
|
49
|
+
"linearGradient", "radialGradient", "stop", "filter", "feGaussianBlur",
|
|
50
|
+
"canvas", "video", "audio", "source", "track", "iframe",
|
|
51
|
+
"br", "hr", "wbr",
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* React hooks that templates must never use.
|
|
56
|
+
*/
|
|
57
|
+
const BANNED_TEMPLATE_HOOKS = new Set([
|
|
58
|
+
"useState", "useEffect", "useMemo", "useCallback", "useRef",
|
|
59
|
+
"useReducer", "useContext", "useLayoutEffect", "useImperativeHandle",
|
|
60
|
+
"useDebugValue", "useDeferredValue", "useTransition", "useId",
|
|
61
|
+
"useSyncExternalStore", "useInsertionEffect",
|
|
62
|
+
"useAssets", "useAssetsOptional", "useEntityData", "useEntityList",
|
|
63
|
+
"useEntityMutations", "useUISlots", "useUIEvents",
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Hook that gets a warning (not error) in templates.
|
|
68
|
+
*/
|
|
69
|
+
const WARNED_TEMPLATE_HOOKS = new Set([
|
|
70
|
+
"useEventBus",
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Generated code paths that should never be imported from.
|
|
75
|
+
*/
|
|
76
|
+
const GENERATED_PATHS = [
|
|
77
|
+
/projects\/[^/]+\/app\//,
|
|
78
|
+
/orbital-rust\/test-output\//,
|
|
79
|
+
/packages\/almadar-patterns\/src\//,
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Generated code paths that should never be committed.
|
|
84
|
+
*/
|
|
85
|
+
const GENERATED_COMMIT_PATHS = [
|
|
86
|
+
"projects/*/app/",
|
|
87
|
+
"orbital-rust/test-output/",
|
|
88
|
+
"apps/builder/packages/client/design-system/",
|
|
89
|
+
"packages/almadar-patterns/src/",
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Check if a file is an entity-aware organism (DataTable, List, Table, etc.).
|
|
94
|
+
*/
|
|
95
|
+
function isEntityAwareOrganism(filename) {
|
|
96
|
+
if (!isOrganism(filename)) return false;
|
|
97
|
+
return /\/(DataTable|List|Table|CardGrid|DetailPanel|MasterDetail)\.(tsx|jsx)$/.test(filename);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if a file is inside an organisms/game/ directory.
|
|
102
|
+
*/
|
|
103
|
+
function isGameOrganism(filename) {
|
|
104
|
+
return /\/organisms\/game\//.test(filename);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Check if a file is a service package source file.
|
|
109
|
+
* Matches packages/almadar-{name}/src/ files, excludes tests and stories.
|
|
110
|
+
*/
|
|
111
|
+
function isServiceFile(filename) {
|
|
112
|
+
if (!/packages\/almadar-[^/]+\/src\//.test(filename)) return false;
|
|
113
|
+
if (/\.(test|spec|stories)\.(ts|tsx|js|jsx)$/.test(filename)) return false;
|
|
114
|
+
if (/\/__tests__\//.test(filename)) return false;
|
|
115
|
+
if (/\/test\//.test(filename)) return false;
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
isTemplateFile,
|
|
121
|
+
isAtomOrMolecule,
|
|
122
|
+
isOrganism,
|
|
123
|
+
isStoryFile,
|
|
124
|
+
isEntityAwareOrganism,
|
|
125
|
+
isGameOrganism,
|
|
126
|
+
isServiceFile,
|
|
127
|
+
BLOCKED_HTML_ELEMENTS,
|
|
128
|
+
ALLOWED_HTML_ELEMENTS,
|
|
129
|
+
BANNED_TEMPLATE_HOOKS,
|
|
130
|
+
WARNED_TEMPLATE_HOOKS,
|
|
131
|
+
GENERATED_PATHS,
|
|
132
|
+
GENERATED_COMMIT_PATHS,
|
|
133
|
+
};
|