@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.
@@ -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
+ };
@@ -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
+ };