@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Almadar Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @almadar/eslint
|
|
2
|
+
|
|
3
|
+
> Custom ESLint rules for Almadar component architecture
|
|
4
|
+
|
|
5
|
+
Part of the [Almadar](https://github.com/almadar-io/almadar) platform.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @almadar/eslint
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { /* ... */ } from '@almadar/eslint';
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## API
|
|
20
|
+
|
|
21
|
+
<!-- Document public exports here -->
|
|
22
|
+
|
|
23
|
+
## License
|
|
24
|
+
|
|
25
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const noRawDomElements = require("./rules/no-raw-dom-elements");
|
|
4
|
+
const templateNoHooks = require("./rules/template-no-hooks");
|
|
5
|
+
const templateNoCallbacks = require("./rules/template-no-callbacks");
|
|
6
|
+
const templateNoLocalVariables = require("./rules/template-no-local-variables");
|
|
7
|
+
const templateNoIteration = require("./rules/template-no-iteration");
|
|
8
|
+
const templateSerializableProps = require("./rules/template-serializable-props");
|
|
9
|
+
const noTypeProp = require("./rules/no-type-prop");
|
|
10
|
+
const noAsAny = require("./rules/no-as-any");
|
|
11
|
+
const noImportGenerated = require("./rules/no-import-generated");
|
|
12
|
+
const requireEventBus = require("./rules/require-event-bus");
|
|
13
|
+
const requireDisplayName = require("./rules/require-display-name");
|
|
14
|
+
const requireClosedCircuitProps = require("./rules/require-closed-circuit-props");
|
|
15
|
+
const requireStories = require("./rules/require-stories");
|
|
16
|
+
const eventNamingConvention = require("./rules/event-naming-convention");
|
|
17
|
+
const templateExtendsBase = require("./rules/template-extends-base");
|
|
18
|
+
const requireTranslate = require("./rules/require-translate");
|
|
19
|
+
const organismNoDataState = require("./rules/organism-no-data-state");
|
|
20
|
+
const organismNoAutoFetch = require("./rules/organism-no-auto-fetch");
|
|
21
|
+
const organismNoCallbackProps = require("./rules/organism-no-callback-props");
|
|
22
|
+
const organismExtendsEntityDisplay = require("./rules/organism-extends-entity-display");
|
|
23
|
+
const organismRenderingStateOnly = require("./rules/organism-rendering-state-only");
|
|
24
|
+
|
|
25
|
+
// Service rules
|
|
26
|
+
const serviceLazySingleton = require("./rules/service-lazy-singleton");
|
|
27
|
+
const serviceSingletonResettable = require("./rules/service-singleton-resettable");
|
|
28
|
+
const serviceNoModuleSideEffects = require("./rules/service-no-module-side-effects");
|
|
29
|
+
const serviceEventbusSingleton = require("./rules/service-eventbus-singleton");
|
|
30
|
+
const serviceEventNameScreaming = require("./rules/service-event-name-screaming");
|
|
31
|
+
const serviceIntegrationExtendsBase = require("./rules/service-integration-extends-base");
|
|
32
|
+
const serviceStoreIsolation = require("./rules/service-store-isolation");
|
|
33
|
+
|
|
34
|
+
const plugin = {
|
|
35
|
+
meta: {
|
|
36
|
+
name: "@almadar/eslint-plugin",
|
|
37
|
+
version: "2.1.0",
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
rules: {
|
|
41
|
+
"no-raw-dom-elements": noRawDomElements,
|
|
42
|
+
"template-no-hooks": templateNoHooks,
|
|
43
|
+
"template-no-callbacks": templateNoCallbacks,
|
|
44
|
+
"template-no-local-variables": templateNoLocalVariables,
|
|
45
|
+
"template-no-iteration": templateNoIteration,
|
|
46
|
+
"template-serializable-props": templateSerializableProps,
|
|
47
|
+
"no-type-prop": noTypeProp,
|
|
48
|
+
"no-as-any": noAsAny,
|
|
49
|
+
"no-import-generated": noImportGenerated,
|
|
50
|
+
"require-event-bus": requireEventBus,
|
|
51
|
+
"require-display-name": requireDisplayName,
|
|
52
|
+
"require-closed-circuit-props": requireClosedCircuitProps,
|
|
53
|
+
"require-stories": requireStories,
|
|
54
|
+
"event-naming-convention": eventNamingConvention,
|
|
55
|
+
"template-extends-base": templateExtendsBase,
|
|
56
|
+
"require-translate": requireTranslate,
|
|
57
|
+
"organism-no-data-state": organismNoDataState,
|
|
58
|
+
"organism-no-auto-fetch": organismNoAutoFetch,
|
|
59
|
+
"organism-no-callback-props": organismNoCallbackProps,
|
|
60
|
+
"organism-extends-entity-display": organismExtendsEntityDisplay,
|
|
61
|
+
"organism-rendering-state-only": organismRenderingStateOnly,
|
|
62
|
+
|
|
63
|
+
// Service rules
|
|
64
|
+
"service-lazy-singleton": serviceLazySingleton,
|
|
65
|
+
"service-singleton-resettable": serviceSingletonResettable,
|
|
66
|
+
"service-no-module-side-effects": serviceNoModuleSideEffects,
|
|
67
|
+
"service-eventbus-singleton": serviceEventbusSingleton,
|
|
68
|
+
"service-event-name-screaming": serviceEventNameScreaming,
|
|
69
|
+
"service-integration-extends-base": serviceIntegrationExtendsBase,
|
|
70
|
+
"service-store-isolation": serviceStoreIsolation,
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
configs: {},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Recommended config — all rules at their intended severity
|
|
77
|
+
plugin.configs.recommended = {
|
|
78
|
+
plugins: {
|
|
79
|
+
almadar: plugin,
|
|
80
|
+
},
|
|
81
|
+
rules: {
|
|
82
|
+
// Error-level — breaks the pipeline or violates hard rules
|
|
83
|
+
"almadar/no-raw-dom-elements": "error",
|
|
84
|
+
"almadar/template-no-hooks": "error",
|
|
85
|
+
"almadar/template-no-callbacks": "error",
|
|
86
|
+
"almadar/template-no-local-variables": "error",
|
|
87
|
+
"almadar/template-no-iteration": "error",
|
|
88
|
+
"almadar/template-serializable-props": "error",
|
|
89
|
+
"almadar/no-type-prop": "error",
|
|
90
|
+
"almadar/no-as-any": "error",
|
|
91
|
+
"almadar/no-import-generated": "error",
|
|
92
|
+
|
|
93
|
+
// Warn-level — conventions that should be followed
|
|
94
|
+
"almadar/require-event-bus": "warn",
|
|
95
|
+
"almadar/require-display-name": "warn",
|
|
96
|
+
"almadar/require-closed-circuit-props": "warn",
|
|
97
|
+
"almadar/require-stories": "warn",
|
|
98
|
+
"almadar/event-naming-convention": "warn",
|
|
99
|
+
"almadar/template-extends-base": "warn",
|
|
100
|
+
"almadar/require-translate": "warn",
|
|
101
|
+
|
|
102
|
+
// Organism rules — enforce dumb organism pattern
|
|
103
|
+
"almadar/organism-no-data-state": "error",
|
|
104
|
+
"almadar/organism-no-auto-fetch": "error",
|
|
105
|
+
"almadar/organism-no-callback-props": "error",
|
|
106
|
+
"almadar/organism-extends-entity-display": "error",
|
|
107
|
+
"almadar/organism-rendering-state-only": "warn",
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Strict config — everything as error
|
|
112
|
+
plugin.configs.strict = {
|
|
113
|
+
plugins: {
|
|
114
|
+
almadar: plugin,
|
|
115
|
+
},
|
|
116
|
+
rules: Object.fromEntries(
|
|
117
|
+
Object.keys(plugin.rules).map((name) => [`almadar/${name}`, "error"])
|
|
118
|
+
),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Templates-only config — just the template rules (for CI on design-system dirs)
|
|
122
|
+
plugin.configs.templates = {
|
|
123
|
+
plugins: {
|
|
124
|
+
almadar: plugin,
|
|
125
|
+
},
|
|
126
|
+
rules: {
|
|
127
|
+
"almadar/template-no-hooks": "error",
|
|
128
|
+
"almadar/template-no-callbacks": "error",
|
|
129
|
+
"almadar/template-no-local-variables": "error",
|
|
130
|
+
"almadar/template-no-iteration": "error",
|
|
131
|
+
"almadar/template-serializable-props": "error",
|
|
132
|
+
"almadar/template-extends-base": "error",
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Organisms-only config — enforce dumb organism pattern
|
|
137
|
+
plugin.configs.organisms = {
|
|
138
|
+
plugins: { almadar: plugin },
|
|
139
|
+
rules: {
|
|
140
|
+
"almadar/organism-no-data-state": "error",
|
|
141
|
+
"almadar/organism-no-auto-fetch": "error",
|
|
142
|
+
"almadar/organism-no-callback-props": "error",
|
|
143
|
+
"almadar/organism-extends-entity-display": "error",
|
|
144
|
+
"almadar/organism-rendering-state-only": "warn",
|
|
145
|
+
"almadar/require-event-bus": "error",
|
|
146
|
+
"almadar/require-closed-circuit-props": "error",
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// Services config — service package structure rules
|
|
151
|
+
plugin.configs.services = {
|
|
152
|
+
plugins: { almadar: plugin },
|
|
153
|
+
rules: {
|
|
154
|
+
"almadar/service-lazy-singleton": "error",
|
|
155
|
+
"almadar/service-singleton-resettable": "error",
|
|
156
|
+
"almadar/service-no-module-side-effects": "error",
|
|
157
|
+
"almadar/service-eventbus-singleton": "error",
|
|
158
|
+
"almadar/service-event-name-screaming": "error",
|
|
159
|
+
"almadar/service-integration-extends-base": "error",
|
|
160
|
+
"almadar/service-store-isolation": "error",
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Services strict config — all service rules as error (same for now, diverges as warn-level rules are added)
|
|
165
|
+
plugin.configs["services-strict"] = {
|
|
166
|
+
plugins: { almadar: plugin },
|
|
167
|
+
rules: {
|
|
168
|
+
"almadar/service-lazy-singleton": "error",
|
|
169
|
+
"almadar/service-singleton-resettable": "error",
|
|
170
|
+
"almadar/service-no-module-side-effects": "error",
|
|
171
|
+
"almadar/service-eventbus-singleton": "error",
|
|
172
|
+
"almadar/service-event-name-screaming": "error",
|
|
173
|
+
"almadar/service-integration-extends-base": "error",
|
|
174
|
+
"almadar/service-store-isolation": "error",
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
module.exports = plugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@almadar/eslint-plugin",
|
|
3
|
+
"version": "2.1.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Custom ESLint rules for Almadar component architecture",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"index.js",
|
|
9
|
+
"rules/",
|
|
10
|
+
"utils/"
|
|
11
|
+
],
|
|
12
|
+
"peerDependencies": {
|
|
13
|
+
"eslint": ">=8.0.0"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/almadar-io/almadar-eslint.git"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const UI_EVENT_PATTERN = /^UI:[A-Z][A-Z0-9_]*$/;
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "suggestion",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Enforce UI:SCREAMING_SNAKE_CASE naming convention for event bus emissions",
|
|
10
|
+
category: "Almadar Architecture",
|
|
11
|
+
},
|
|
12
|
+
messages: {
|
|
13
|
+
badEventName:
|
|
14
|
+
"Event name '{{name}}' should follow the UI:SCREAMING_SNAKE_CASE pattern (e.g., 'UI:SAVE', 'UI:CONFIRM_DELETE').",
|
|
15
|
+
},
|
|
16
|
+
schema: [],
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
create(context) {
|
|
20
|
+
return {
|
|
21
|
+
CallExpression(node) {
|
|
22
|
+
const callee = node.callee;
|
|
23
|
+
|
|
24
|
+
// Match: emit('...'), eventBus.emit('...')
|
|
25
|
+
let isEmit = false;
|
|
26
|
+
if (
|
|
27
|
+
callee.type === "Identifier" &&
|
|
28
|
+
callee.name === "emit"
|
|
29
|
+
) {
|
|
30
|
+
isEmit = true;
|
|
31
|
+
} else if (
|
|
32
|
+
callee.type === "MemberExpression" &&
|
|
33
|
+
callee.property.type === "Identifier" &&
|
|
34
|
+
callee.property.name === "emit"
|
|
35
|
+
) {
|
|
36
|
+
isEmit = true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!isEmit || node.arguments.length === 0) return;
|
|
40
|
+
|
|
41
|
+
const firstArg = node.arguments[0];
|
|
42
|
+
|
|
43
|
+
// Check string literal: emit('UI:SAVE')
|
|
44
|
+
if (firstArg.type === "Literal" && typeof firstArg.value === "string") {
|
|
45
|
+
const eventName = firstArg.value;
|
|
46
|
+
// Only check events that look like they should be UI events
|
|
47
|
+
// (skip internal system events that don't start with UI:)
|
|
48
|
+
if (eventName.startsWith("UI:") && !UI_EVENT_PATTERN.test(eventName)) {
|
|
49
|
+
context.report({
|
|
50
|
+
node: firstArg,
|
|
51
|
+
messageId: "badEventName",
|
|
52
|
+
data: { name: eventName },
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check template literal: emit(`UI:${action}`)
|
|
58
|
+
if (firstArg.type === "TemplateLiteral") {
|
|
59
|
+
// If the static part starts with "UI:" we trust the dynamic part
|
|
60
|
+
// This pattern is used inside atoms: emit(`UI:${action}`, ...)
|
|
61
|
+
// No need to flag these — the action prop is validated elsewhere
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
meta: {
|
|
5
|
+
type: "problem",
|
|
6
|
+
docs: {
|
|
7
|
+
description: "Disallow 'as any' type casts — fix the actual type issue instead",
|
|
8
|
+
category: "Almadar Architecture",
|
|
9
|
+
},
|
|
10
|
+
messages: {
|
|
11
|
+
noAsAny:
|
|
12
|
+
"Never use 'as any'. Fix the actual type issue: add missing fields, use proper interfaces, or create type-safe helpers.",
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
create(context) {
|
|
18
|
+
return {
|
|
19
|
+
TSAsExpression(node) {
|
|
20
|
+
const typeAnnotation = node.typeAnnotation;
|
|
21
|
+
if (
|
|
22
|
+
typeAnnotation &&
|
|
23
|
+
typeAnnotation.type === "TSAnyKeyword"
|
|
24
|
+
) {
|
|
25
|
+
context.report({ node, messageId: "noAsAny" });
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { GENERATED_PATHS } = require("../utils/helpers");
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Disallow imports from generated/compiled code paths",
|
|
10
|
+
category: "Almadar Architecture",
|
|
11
|
+
},
|
|
12
|
+
messages: {
|
|
13
|
+
noImportGenerated:
|
|
14
|
+
"Do not import from generated code ('{{source}}'). Import from the source instead (design-system, packages, etc.).",
|
|
15
|
+
},
|
|
16
|
+
schema: [],
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
create(context) {
|
|
20
|
+
function checkSource(node, source) {
|
|
21
|
+
if (!source || !source.value) return;
|
|
22
|
+
const importPath = source.value;
|
|
23
|
+
|
|
24
|
+
for (const pattern of GENERATED_PATHS) {
|
|
25
|
+
if (pattern.test(importPath)) {
|
|
26
|
+
context.report({
|
|
27
|
+
node,
|
|
28
|
+
messageId: "noImportGenerated",
|
|
29
|
+
data: { source: importPath },
|
|
30
|
+
});
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
ImportDeclaration(node) {
|
|
38
|
+
checkSource(node, node.source);
|
|
39
|
+
},
|
|
40
|
+
// Dynamic imports: import('...')
|
|
41
|
+
ImportExpression(node) {
|
|
42
|
+
checkSource(node, node.source);
|
|
43
|
+
},
|
|
44
|
+
// require('...')
|
|
45
|
+
CallExpression(node) {
|
|
46
|
+
if (
|
|
47
|
+
node.callee.type === "Identifier" &&
|
|
48
|
+
node.callee.name === "require" &&
|
|
49
|
+
node.arguments.length > 0 &&
|
|
50
|
+
node.arguments[0].type === "Literal"
|
|
51
|
+
) {
|
|
52
|
+
checkSource(node, node.arguments[0]);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { BLOCKED_HTML_ELEMENTS, ALLOWED_HTML_ELEMENTS, isAtomOrMolecule, isStoryFile } = require("../utils/helpers");
|
|
4
|
+
|
|
5
|
+
const ELEMENT_REPLACEMENTS = {
|
|
6
|
+
div: "Box, VStack, HStack, Stack",
|
|
7
|
+
span: "Typography, Badge, TextHighlight",
|
|
8
|
+
p: "Typography (variant='body1')",
|
|
9
|
+
h1: "Typography (variant='h1')",
|
|
10
|
+
h2: "Typography (variant='h2')",
|
|
11
|
+
h3: "Typography (variant='h3')",
|
|
12
|
+
h4: "Typography (variant='h4')",
|
|
13
|
+
h5: "Typography (variant='h5')",
|
|
14
|
+
h6: "Typography (variant='h6')",
|
|
15
|
+
button: "Button",
|
|
16
|
+
input: "Input, Checkbox, Radio, Switch",
|
|
17
|
+
textarea: "Textarea",
|
|
18
|
+
select: "Select, RelationSelect",
|
|
19
|
+
img: "Avatar or Box with background",
|
|
20
|
+
ul: "VStack with mapped children, or List",
|
|
21
|
+
ol: "VStack with mapped children, or List",
|
|
22
|
+
li: "Box or HStack",
|
|
23
|
+
table: "DataTable, Table",
|
|
24
|
+
tr: "DataTable, Table",
|
|
25
|
+
td: "DataTable, Table",
|
|
26
|
+
th: "DataTable, Table",
|
|
27
|
+
form: "FormSection, Form",
|
|
28
|
+
section: "Box, VStack, Section",
|
|
29
|
+
main: "Box, VStack",
|
|
30
|
+
article: "Box, VStack",
|
|
31
|
+
aside: "Box, SidePanel",
|
|
32
|
+
nav: "Navigation, Breadcrumb, Tabs",
|
|
33
|
+
header: "Box, HStack, PageHeader",
|
|
34
|
+
footer: "Box, HStack",
|
|
35
|
+
label: "Label, FormField",
|
|
36
|
+
a: "Button (with navigatesTo) or Typography (with link)",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
meta: {
|
|
41
|
+
type: "problem",
|
|
42
|
+
docs: {
|
|
43
|
+
description: "Disallow raw HTML elements — use @almadar/ui components instead",
|
|
44
|
+
category: "Almadar Architecture",
|
|
45
|
+
},
|
|
46
|
+
messages: {
|
|
47
|
+
noRawElement:
|
|
48
|
+
"Use @almadar/ui components instead of <{{element}}>. Suggested: {{replacement}}.",
|
|
49
|
+
},
|
|
50
|
+
schema: [],
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
create(context) {
|
|
54
|
+
const filename = context.filename;
|
|
55
|
+
|
|
56
|
+
// Atoms and molecules are allowed to use raw elements (they implement primitives)
|
|
57
|
+
if (isAtomOrMolecule(filename)) return {};
|
|
58
|
+
|
|
59
|
+
// Stories are allowed to use raw elements for demo wrappers
|
|
60
|
+
if (isStoryFile(filename)) return {};
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
JSXOpeningElement(node) {
|
|
64
|
+
const name = node.name;
|
|
65
|
+
|
|
66
|
+
// Only check simple element names (not member expressions like Foo.Bar)
|
|
67
|
+
if (name.type !== "JSXIdentifier") return;
|
|
68
|
+
|
|
69
|
+
const elementName = name.name;
|
|
70
|
+
|
|
71
|
+
// Skip allowed elements (SVG, canvas, media)
|
|
72
|
+
if (ALLOWED_HTML_ELEMENTS.has(elementName)) return;
|
|
73
|
+
|
|
74
|
+
// Flag blocked elements
|
|
75
|
+
if (BLOCKED_HTML_ELEMENTS.has(elementName)) {
|
|
76
|
+
context.report({
|
|
77
|
+
node,
|
|
78
|
+
messageId: "noRawElement",
|
|
79
|
+
data: {
|
|
80
|
+
element: elementName,
|
|
81
|
+
replacement: ELEMENT_REPLACEMENTS[elementName] || "an @almadar/ui component",
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
meta: {
|
|
5
|
+
type: "problem",
|
|
6
|
+
docs: {
|
|
7
|
+
description: "Disallow 'type' as a component prop name — reserved for pattern type identifiers",
|
|
8
|
+
category: "Almadar Architecture",
|
|
9
|
+
},
|
|
10
|
+
messages: {
|
|
11
|
+
noTypeProp:
|
|
12
|
+
"The 'type' prop is reserved for pattern type identifiers. Use a descriptive name instead: chartType, inputType, alertType, documentType, etc.",
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
create(context) {
|
|
18
|
+
return {
|
|
19
|
+
// Check TypeScript interfaces: interface FooProps { type: ... }
|
|
20
|
+
TSPropertySignature(node) {
|
|
21
|
+
if (
|
|
22
|
+
node.key &&
|
|
23
|
+
node.key.type === "Identifier" &&
|
|
24
|
+
node.key.name === "type"
|
|
25
|
+
) {
|
|
26
|
+
// Only flag if the parent interface name ends with "Props"
|
|
27
|
+
const parent = node.parent;
|
|
28
|
+
if (
|
|
29
|
+
parent &&
|
|
30
|
+
parent.parent &&
|
|
31
|
+
parent.parent.type === "TSInterfaceDeclaration" &&
|
|
32
|
+
parent.parent.id &&
|
|
33
|
+
/Props$/.test(parent.parent.id.name)
|
|
34
|
+
) {
|
|
35
|
+
context.report({ node, messageId: "noTypeProp" });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
// Check type aliases: type FooProps = { type: ... }
|
|
41
|
+
TSTypeAliasDeclaration(node) {
|
|
42
|
+
if (!node.id || !/Props$/.test(node.id.name)) return;
|
|
43
|
+
|
|
44
|
+
const typeAnnotation = node.typeAnnotation;
|
|
45
|
+
if (typeAnnotation && typeAnnotation.type === "TSTypeLiteral") {
|
|
46
|
+
for (const member of typeAnnotation.members) {
|
|
47
|
+
if (
|
|
48
|
+
member.type === "TSPropertySignature" &&
|
|
49
|
+
member.key &&
|
|
50
|
+
member.key.type === "Identifier" &&
|
|
51
|
+
member.key.name === "type"
|
|
52
|
+
) {
|
|
53
|
+
context.report({ node: member, messageId: "noTypeProp" });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const { isOrganism, isStoryFile } = require("../utils/helpers");
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
meta: {
|
|
6
|
+
type: "problem",
|
|
7
|
+
docs: {
|
|
8
|
+
description: "Require organisms with entity/data props to extend EntityDisplayProps",
|
|
9
|
+
category: "Almadar Architecture",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
mustExtend: "'{{interfaceName}}' has '{{propName}}' prop but does not extend EntityDisplayProps. Extend it to share the base contract.",
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
const filename = context.filename || context.getFilename();
|
|
18
|
+
if (!isOrganism(filename) || isStoryFile(filename)) return {};
|
|
19
|
+
|
|
20
|
+
// Whitelist Form.tsx (documented exception)
|
|
21
|
+
if (/\/Form\.tsx$/.test(filename)) return {};
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
TSInterfaceDeclaration(node) {
|
|
25
|
+
const interfaceName = node.id?.name;
|
|
26
|
+
if (!interfaceName?.endsWith("Props")) return;
|
|
27
|
+
|
|
28
|
+
// Check if it has entity or data prop
|
|
29
|
+
const hasEntityOrData = node.body?.body?.some(
|
|
30
|
+
(member) =>
|
|
31
|
+
member.type === "TSPropertySignature" &&
|
|
32
|
+
member.key?.type === "Identifier" &&
|
|
33
|
+
(member.key.name === "entity" || member.key.name === "data")
|
|
34
|
+
);
|
|
35
|
+
if (!hasEntityOrData) return;
|
|
36
|
+
|
|
37
|
+
// Check if it extends EntityDisplayProps
|
|
38
|
+
const extendsEntityDisplay = node.extends?.some((ext) => {
|
|
39
|
+
const typeName = ext.expression?.type === "Identifier"
|
|
40
|
+
? ext.expression.name
|
|
41
|
+
: ext.expression?.type === "TSTypeReference"
|
|
42
|
+
? ext.expression.typeName?.name
|
|
43
|
+
: null;
|
|
44
|
+
return typeName === "EntityDisplayProps";
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (!extendsEntityDisplay) {
|
|
48
|
+
const dataProp = node.body?.body?.find(
|
|
49
|
+
(m) => m.type === "TSPropertySignature" && m.key?.type === "Identifier" && (m.key.name === "entity" || m.key.name === "data")
|
|
50
|
+
);
|
|
51
|
+
context.report({
|
|
52
|
+
node: dataProp || node,
|
|
53
|
+
messageId: "mustExtend",
|
|
54
|
+
data: {
|
|
55
|
+
interfaceName,
|
|
56
|
+
propName: dataProp?.key?.name || "entity/data",
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const { isOrganism, isStoryFile } = require("../utils/helpers");
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
meta: {
|
|
6
|
+
type: "problem",
|
|
7
|
+
docs: {
|
|
8
|
+
description: "Disallow auto-fetch hooks in organisms",
|
|
9
|
+
category: "Almadar Architecture",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
noAutoFetch: "Organism must not call '{{hookName}}'. Receive data via the 'data' prop from the trait's render-ui instead.",
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
const filename = context.filename || context.getFilename();
|
|
18
|
+
if (!isOrganism(filename) || isStoryFile(filename)) return {};
|
|
19
|
+
|
|
20
|
+
const BANNED_HOOKS = new Set([
|
|
21
|
+
"useEntityList",
|
|
22
|
+
"useEntityDetail",
|
|
23
|
+
"usePaginatedEntityList",
|
|
24
|
+
"useEntityListSuspense",
|
|
25
|
+
"useEntitySuspense",
|
|
26
|
+
"useSelectedEntity",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
CallExpression(node) {
|
|
31
|
+
let hookName = null;
|
|
32
|
+
if (node.callee.type === "Identifier") {
|
|
33
|
+
hookName = node.callee.name;
|
|
34
|
+
} else if (node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier") {
|
|
35
|
+
hookName = node.callee.property.name;
|
|
36
|
+
}
|
|
37
|
+
if (hookName && BANNED_HOOKS.has(hookName)) {
|
|
38
|
+
context.report({
|
|
39
|
+
node,
|
|
40
|
+
messageId: "noAutoFetch",
|
|
41
|
+
data: { hookName },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
};
|