@almadar/eslint-plugin 2.2.0 → 2.3.0

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 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");
@@ -23,7 +24,7 @@ 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");
25
26
 
26
- // Service rules
27
+ // Service rules - Phase 1-3 (Implemented)
27
28
  const serviceLazySingleton = require("./rules/service-lazy-singleton");
28
29
  const serviceSingletonResettable = require("./rules/service-singleton-resettable");
29
30
  const serviceNoModuleSideEffects = require("./rules/service-no-module-side-effects");
@@ -32,6 +33,16 @@ const serviceEventNameScreaming = require("./rules/service-event-name-screaming"
32
33
  const serviceIntegrationExtendsBase = require("./rules/service-integration-extends-base");
33
34
  const serviceStoreIsolation = require("./rules/service-store-isolation");
34
35
 
36
+ // Service rules - Phase 4 (New)
37
+ const serviceBarrelComplete = require("./rules/service-barrel-complete");
38
+ const serviceSubpathMatchTsup = require("./rules/service-subpath-match-tsup");
39
+ const serviceNoWorkspaceRefs = require("./rules/service-no-workspace-refs");
40
+ const serviceExternalDeclared = require("./rules/service-external-declared");
41
+
42
+ // Service rules - Additional (New)
43
+ const serviceUseEventbus = require("./rules/service-use-eventbus");
44
+ const serviceNoInlineQueries = require("./rules/service-no-inline-queries");
45
+
35
46
  const plugin = {
36
47
  meta: {
37
48
  name: "@almadar/eslint-plugin",
@@ -47,6 +58,7 @@ const plugin = {
47
58
  "template-serializable-props": templateSerializableProps,
48
59
  "no-type-prop": noTypeProp,
49
60
  "no-as-any": noAsAny,
61
+ "no-entity-string-type": noEntityStringType,
50
62
  "no-import-generated": noImportGenerated,
51
63
  "no-use-navigate": noUseNavigate,
52
64
  "require-event-bus": requireEventBus,
@@ -62,7 +74,7 @@ const plugin = {
62
74
  "organism-extends-entity-display": organismExtendsEntityDisplay,
63
75
  "organism-rendering-state-only": organismRenderingStateOnly,
64
76
 
65
- // Service rules
77
+ // Service rules - Phase 1-3
66
78
  "service-lazy-singleton": serviceLazySingleton,
67
79
  "service-singleton-resettable": serviceSingletonResettable,
68
80
  "service-no-module-side-effects": serviceNoModuleSideEffects,
@@ -70,6 +82,16 @@ const plugin = {
70
82
  "service-event-name-screaming": serviceEventNameScreaming,
71
83
  "service-integration-extends-base": serviceIntegrationExtendsBase,
72
84
  "service-store-isolation": serviceStoreIsolation,
85
+
86
+ // Service rules - Phase 4
87
+ "service-barrel-complete": serviceBarrelComplete,
88
+ "service-subpath-match-tsup": serviceSubpathMatchTsup,
89
+ "service-no-workspace-refs": serviceNoWorkspaceRefs,
90
+ "service-external-declared": serviceExternalDeclared,
91
+
92
+ // Service rules - Additional
93
+ "service-use-eventbus": serviceUseEventbus,
94
+ "service-no-inline-queries": serviceNoInlineQueries,
73
95
  },
74
96
 
75
97
  configs: {},
@@ -90,6 +112,7 @@ plugin.configs.recommended = {
90
112
  "almadar/template-serializable-props": "error",
91
113
  "almadar/no-type-prop": "error",
92
114
  "almadar/no-as-any": "error",
115
+ "almadar/no-entity-string-type": "error",
93
116
  "almadar/no-import-generated": "error",
94
117
  "almadar/no-use-navigate": "error",
95
118
 
@@ -154,6 +177,7 @@ plugin.configs.organisms = {
154
177
  plugin.configs.services = {
155
178
  plugins: { almadar: plugin },
156
179
  rules: {
180
+ // Phase 1-3
157
181
  "almadar/service-lazy-singleton": "error",
158
182
  "almadar/service-singleton-resettable": "error",
159
183
  "almadar/service-no-module-side-effects": "error",
@@ -161,13 +185,22 @@ plugin.configs.services = {
161
185
  "almadar/service-event-name-screaming": "error",
162
186
  "almadar/service-integration-extends-base": "error",
163
187
  "almadar/service-store-isolation": "error",
188
+ // Phase 4
189
+ "almadar/service-barrel-complete": "warn",
190
+ "almadar/service-subpath-match-tsup": "warn",
191
+ "almadar/service-no-workspace-refs": "error",
192
+ "almadar/service-external-declared": "error",
193
+ // Additional
194
+ "almadar/service-use-eventbus": "warn",
195
+ "almadar/service-no-inline-queries": "error",
164
196
  },
165
197
  };
166
198
 
167
- // Services strict config — all service rules as error (same for now, diverges as warn-level rules are added)
199
+ // Services strict config — all service rules as error
168
200
  plugin.configs["services-strict"] = {
169
201
  plugins: { almadar: plugin },
170
202
  rules: {
203
+ // Phase 1-3
171
204
  "almadar/service-lazy-singleton": "error",
172
205
  "almadar/service-singleton-resettable": "error",
173
206
  "almadar/service-no-module-side-effects": "error",
@@ -175,6 +208,14 @@ plugin.configs["services-strict"] = {
175
208
  "almadar/service-event-name-screaming": "error",
176
209
  "almadar/service-integration-extends-base": "error",
177
210
  "almadar/service-store-isolation": "error",
211
+ // Phase 4
212
+ "almadar/service-barrel-complete": "error",
213
+ "almadar/service-subpath-match-tsup": "error",
214
+ "almadar/service-no-workspace-refs": "error",
215
+ "almadar/service-external-declared": "error",
216
+ // Additional
217
+ "almadar/service-use-eventbus": "error",
218
+ "almadar/service-no-inline-queries": "error",
178
219
  },
179
220
  };
180
221
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@almadar/eslint-plugin",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "private": false,
5
5
  "description": "Custom ESLint rules for Almadar component architecture",
6
6
  "main": "index.js",
@@ -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
+ };
@@ -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", "data", "sortBy", "sortDirection", "page", "pageSize", "totalCount", "searchValue"];
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
+ const ignorePrivate = options.ignorePrivate !== false;
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 (err) {
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 [name, 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,93 @@
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, exportConfig] of Object.entries(exports)) {
71
+ if (exportPath.startsWith("./")) {
72
+ const expectedPattern = exportPath === "."
73
+ ? "index.js"
74
+ : `${exportPath.replace(/^\.\//, "/")}.js`;
75
+
76
+ // Check if there's a matching entry in tsup
77
+ const hasMatchingEntry = tsupContent.includes(exportPath.replace(/^\.\//, "src/")) ||
78
+ tsupContent.includes(exportPath.replace(/^\.\//, "")) ||
79
+ (exportPath === "." && tsupContent.includes("src/index"));
80
+
81
+ if (!hasMatchingEntry) {
82
+ context.report({
83
+ node,
84
+ messageId: "exportMismatch",
85
+ data: { exportPath },
86
+ });
87
+ }
88
+ }
89
+ }
90
+ },
91
+ };
92
+ },
93
+ };
@@ -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
+ };