@blumintinc/eslint-plugin-blumint 1.0.5 → 1.1.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.
Files changed (37) hide show
  1. package/README.md +1 -0
  2. package/lib/index.js +44 -2
  3. package/lib/rules/consistent-callback-naming.d.ts +2 -0
  4. package/lib/rules/consistent-callback-naming.js +84 -0
  5. package/lib/rules/enforce-callable-types.d.ts +3 -0
  6. package/lib/rules/enforce-callable-types.js +97 -0
  7. package/lib/rules/enforce-callback-memo.d.ts +3 -0
  8. package/lib/rules/enforce-callback-memo.js +79 -0
  9. package/lib/rules/enforce-dynamic-firebase-imports.d.ts +4 -0
  10. package/lib/rules/enforce-dynamic-firebase-imports.js +49 -0
  11. package/lib/rules/enforce-identifiable-firestore-type.d.ts +3 -0
  12. package/lib/rules/enforce-identifiable-firestore-type.js +89 -0
  13. package/lib/rules/enforce-serializable-params.d.ts +9 -0
  14. package/lib/rules/enforce-serializable-params.js +127 -0
  15. package/lib/rules/global-const-style.d.ts +5 -0
  16. package/lib/rules/global-const-style.js +77 -0
  17. package/lib/rules/no-jsx-whitespace-literal.d.ts +1 -0
  18. package/lib/rules/no-jsx-whitespace-literal.js +34 -0
  19. package/lib/rules/no-unused-props.d.ts +6 -0
  20. package/lib/rules/no-unused-props.js +91 -0
  21. package/lib/rules/require-dynamic-firebase-imports.d.ts +6 -0
  22. package/lib/rules/require-dynamic-firebase-imports.js +84 -0
  23. package/lib/rules/require-https-error.d.ts +6 -0
  24. package/lib/rules/require-https-error.js +74 -0
  25. package/lib/rules/require-image-overlayed.d.ts +7 -0
  26. package/lib/rules/require-image-overlayed.js +80 -0
  27. package/lib/rules/require-memo.js +3 -3
  28. package/lib/rules/require-usememo-object-literals.d.ts +4 -0
  29. package/lib/rules/require-usememo-object-literals.js +53 -0
  30. package/lib/rules/use-custom-link.d.ts +1 -0
  31. package/lib/rules/use-custom-link.js +49 -0
  32. package/lib/rules/use-custom-router.d.ts +1 -0
  33. package/lib/rules/use-custom-router.js +65 -0
  34. package/lib/utils/ASTHelpers.js +2 -0
  35. package/lib/utils/graph/ClassGraphBuilder.js +4 -0
  36. package/lib/utils/graph/ClassGraphSorterReadability.js +8 -3
  37. package/package.json +2 -2
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const utils_1 = require("@typescript-eslint/utils");
4
+ const createRule_1 = require("../utils/createRule");
5
+ const NON_SERIALIZABLE_TYPES = new Set([
6
+ 'Date',
7
+ 'DocumentReference',
8
+ 'Timestamp',
9
+ 'Map',
10
+ 'Set',
11
+ 'Symbol',
12
+ 'Function',
13
+ 'undefined',
14
+ ]);
15
+ exports.default = (0, createRule_1.createRule)({
16
+ name: 'enforce-serializable-params',
17
+ meta: {
18
+ type: 'problem',
19
+ docs: {
20
+ description: 'Enforce serializable parameters in Firebase Callable/HTTPS Cloud Functions',
21
+ recommended: 'error',
22
+ },
23
+ schema: [
24
+ {
25
+ type: 'object',
26
+ properties: {
27
+ additionalNonSerializableTypes: {
28
+ type: 'array',
29
+ items: { type: 'string' },
30
+ },
31
+ functionTypes: {
32
+ type: 'array',
33
+ items: { type: 'string' },
34
+ default: ['CallableRequest'],
35
+ },
36
+ },
37
+ additionalProperties: false,
38
+ },
39
+ ],
40
+ messages: {
41
+ nonSerializableParam: 'Parameter type "{{ type }}" is not serializable',
42
+ nonSerializableProperty: 'Property "{{ prop }}" has non-serializable type "{{ type }}"',
43
+ },
44
+ },
45
+ defaultOptions: [
46
+ {
47
+ additionalNonSerializableTypes: [],
48
+ functionTypes: ['CallableRequest'],
49
+ },
50
+ ],
51
+ create(context, [options]) {
52
+ const allNonSerializableTypes = new Set([
53
+ ...NON_SERIALIZABLE_TYPES,
54
+ ...(options.additionalNonSerializableTypes || []),
55
+ ]);
56
+ const typeAliasMap = new Map();
57
+ function isNonSerializableType(typeName) {
58
+ return allNonSerializableTypes.has(typeName);
59
+ }
60
+ function checkTypeNode(node, propName) {
61
+ if (!node)
62
+ return;
63
+ switch (node.type) {
64
+ case utils_1.AST_NODE_TYPES.TSTypeReference: {
65
+ const typeName = node.typeName.name;
66
+ if (isNonSerializableType(typeName)) {
67
+ context.report({
68
+ node,
69
+ messageId: propName
70
+ ? 'nonSerializableProperty'
71
+ : 'nonSerializableParam',
72
+ data: {
73
+ type: typeName,
74
+ prop: propName,
75
+ },
76
+ });
77
+ }
78
+ // Check type parameters of generic types (like Array<T>)
79
+ if (node.typeParameters) {
80
+ node.typeParameters.params.forEach((param) => checkTypeNode(param, propName));
81
+ }
82
+ break;
83
+ }
84
+ case utils_1.AST_NODE_TYPES.TSArrayType:
85
+ checkTypeNode(node.elementType, propName);
86
+ break;
87
+ case utils_1.AST_NODE_TYPES.TSTypeAnnotation:
88
+ checkTypeNode(node.typeAnnotation, propName);
89
+ break;
90
+ case utils_1.AST_NODE_TYPES.TSTypeLiteral:
91
+ node.members.forEach((member) => {
92
+ if (member.type === utils_1.AST_NODE_TYPES.TSPropertySignature) {
93
+ const propertyName = member.key.name;
94
+ checkTypeNode(member.typeAnnotation, propertyName);
95
+ }
96
+ });
97
+ break;
98
+ case utils_1.AST_NODE_TYPES.TSUnionType:
99
+ node.types.forEach((type) => checkTypeNode(type, propName));
100
+ break;
101
+ }
102
+ }
103
+ return {
104
+ TSTypeAliasDeclaration(node) {
105
+ typeAliasMap.set(node.id.name, node);
106
+ },
107
+ TSTypeReference(node) {
108
+ const typeName = node.typeName.name;
109
+ if (options.functionTypes.includes(typeName) &&
110
+ node.typeParameters?.params[0]) {
111
+ const typeParam = node.typeParameters.params[0];
112
+ if (typeParam.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
113
+ const referencedTypeName = typeParam.typeName.name;
114
+ const typeAlias = typeAliasMap.get(referencedTypeName);
115
+ if (typeAlias) {
116
+ checkTypeNode(typeAlias.typeAnnotation);
117
+ }
118
+ }
119
+ else {
120
+ checkTypeNode(typeParam);
121
+ }
122
+ }
123
+ },
124
+ };
125
+ },
126
+ });
127
+ //# sourceMappingURL=enforce-serializable-params.js.map
@@ -0,0 +1,5 @@
1
+ import { TSESTree } from '@typescript-eslint/utils';
2
+ declare const _default: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"upperSnakeCase" | "asConst", never[], {
3
+ VariableDeclaration(node: TSESTree.VariableDeclaration): void;
4
+ }>;
5
+ export default _default;
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const utils_1 = require("@typescript-eslint/utils");
4
+ const createRule_1 = require("../utils/createRule");
5
+ const isUpperSnakeCase = (str) => /^[A-Z][A-Z0-9_]*$/.test(str);
6
+ exports.default = (0, createRule_1.createRule)({
7
+ name: 'global-const-style',
8
+ meta: {
9
+ type: 'suggestion',
10
+ docs: {
11
+ description: 'Enforce UPPER_SNAKE_CASE and as const for global static constants',
12
+ recommended: 'error',
13
+ },
14
+ fixable: 'code',
15
+ schema: [],
16
+ messages: {
17
+ upperSnakeCase: 'Global constants should be in UPPER_SNAKE_CASE',
18
+ asConst: 'Global constants should use "as const"',
19
+ },
20
+ },
21
+ defaultOptions: [],
22
+ create(context) {
23
+ return {
24
+ VariableDeclaration(node) {
25
+ // Only check top-level const declarations
26
+ if (node.kind !== 'const' ||
27
+ node.parent?.type !== utils_1.AST_NODE_TYPES.Program) {
28
+ return;
29
+ }
30
+ node.declarations.forEach((declaration) => {
31
+ // Skip destructuring patterns
32
+ if (declaration.id.type !== utils_1.AST_NODE_TYPES.Identifier) {
33
+ return;
34
+ }
35
+ const { name } = declaration.id;
36
+ const init = declaration.init;
37
+ // Skip if no initializer or if it's a dynamic value
38
+ if (!init ||
39
+ init.type === utils_1.AST_NODE_TYPES.CallExpression ||
40
+ init.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
41
+ return;
42
+ }
43
+ // Check for UPPER_SNAKE_CASE
44
+ if (!isUpperSnakeCase(name)) {
45
+ context.report({
46
+ node: declaration.id,
47
+ messageId: 'upperSnakeCase',
48
+ fix(fixer) {
49
+ const newName = name
50
+ .replace(/([A-Z])/g, '_$1')
51
+ .toUpperCase()
52
+ .replace(/^_/, '');
53
+ return fixer.replaceText(declaration.id, newName);
54
+ },
55
+ });
56
+ }
57
+ // Check for as const
58
+ if (init.type !== utils_1.AST_NODE_TYPES.TSAsExpression ||
59
+ init.typeAnnotation.type !== utils_1.AST_NODE_TYPES.TSTypeReference ||
60
+ init.typeAnnotation.typeName.name !==
61
+ 'const') {
62
+ context.report({
63
+ node: init,
64
+ messageId: 'asConst',
65
+ fix(fixer) {
66
+ const sourceCode = context.getSourceCode();
67
+ const initText = sourceCode.getText(init);
68
+ return fixer.replaceText(init, `${initText} as const`);
69
+ },
70
+ });
71
+ }
72
+ });
73
+ },
74
+ };
75
+ },
76
+ });
77
+ //# sourceMappingURL=global-const-style.js.map
@@ -0,0 +1 @@
1
+ export declare const noJsxWhitespaceLiteral: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"noWhitespaceLiteral", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.noJsxWhitespaceLiteral = void 0;
4
+ const createRule_1 = require("../utils/createRule");
5
+ exports.noJsxWhitespaceLiteral = (0, createRule_1.createRule)({
6
+ name: 'no-jsx-whitespace-literal',
7
+ meta: {
8
+ type: 'suggestion',
9
+ docs: {
10
+ description: 'Disallow the use of {" "} elements in JSX code',
11
+ recommended: 'error',
12
+ },
13
+ schema: [],
14
+ messages: {
15
+ noWhitespaceLiteral: 'Avoid using {" "} for spacing in JSX. Use proper text nodes or CSS spacing instead.',
16
+ },
17
+ },
18
+ defaultOptions: [],
19
+ create(context) {
20
+ return {
21
+ JSXExpressionContainer(node) {
22
+ if (node.expression.type === 'Literal' &&
23
+ typeof node.expression.value === 'string' &&
24
+ node.expression.value.trim() === '') {
25
+ context.report({
26
+ node,
27
+ messageId: 'noWhitespaceLiteral',
28
+ });
29
+ }
30
+ },
31
+ };
32
+ },
33
+ });
34
+ //# sourceMappingURL=no-jsx-whitespace-literal.js.map
@@ -0,0 +1,6 @@
1
+ import { TSESTree } from '@typescript-eslint/utils';
2
+ export declare const noUnusedProps: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"unusedProp", never[], {
3
+ TSTypeAliasDeclaration(node: TSESTree.TSTypeAliasDeclaration): void;
4
+ VariableDeclaration(node: TSESTree.VariableDeclaration): void;
5
+ 'VariableDeclaration:exit'(node: never): void;
6
+ }>;
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.noUnusedProps = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ exports.noUnusedProps = (0, createRule_1.createRule)({
7
+ name: 'no-unused-props',
8
+ meta: {
9
+ type: 'problem',
10
+ docs: {
11
+ description: 'Detect unused props in React component type definitions',
12
+ recommended: 'error',
13
+ },
14
+ schema: [],
15
+ messages: {
16
+ unusedProp: 'Prop "{{propName}}" is defined in type but not used in component',
17
+ },
18
+ fixable: 'code',
19
+ },
20
+ defaultOptions: [],
21
+ create(context) {
22
+ const propsTypes = new Map();
23
+ const usedProps = new Map();
24
+ let currentComponent = null;
25
+ return {
26
+ TSTypeAliasDeclaration(node) {
27
+ if (node.id.name.endsWith('Props')) {
28
+ if (node.typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
29
+ const props = {};
30
+ node.typeAnnotation.members.forEach((member) => {
31
+ if (member.type === utils_1.AST_NODE_TYPES.TSPropertySignature &&
32
+ member.key.type === utils_1.AST_NODE_TYPES.Identifier) {
33
+ props[member.key.name] = member.key;
34
+ }
35
+ });
36
+ propsTypes.set(node.id.name, props);
37
+ }
38
+ }
39
+ },
40
+ VariableDeclaration(node) {
41
+ if (node.declarations.length === 1) {
42
+ const declaration = node.declarations[0];
43
+ if (declaration.init?.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
44
+ const param = declaration.init.params[0];
45
+ if (param?.type === utils_1.AST_NODE_TYPES.ObjectPattern &&
46
+ param.typeAnnotation?.typeAnnotation.type ===
47
+ utils_1.AST_NODE_TYPES.TSTypeReference &&
48
+ param.typeAnnotation.typeAnnotation.typeName.type ===
49
+ utils_1.AST_NODE_TYPES.Identifier) {
50
+ const typeName = param.typeAnnotation.typeAnnotation.typeName.name;
51
+ if (typeName.endsWith('Props')) {
52
+ currentComponent = { node, typeName };
53
+ const used = new Set();
54
+ param.properties.forEach((prop) => {
55
+ if (prop.type === utils_1.AST_NODE_TYPES.Property &&
56
+ prop.key.type === utils_1.AST_NODE_TYPES.Identifier) {
57
+ used.add(prop.key.name);
58
+ }
59
+ });
60
+ usedProps.set(typeName, used);
61
+ }
62
+ }
63
+ }
64
+ }
65
+ },
66
+ 'VariableDeclaration:exit'(node) {
67
+ if (currentComponent?.node === node) {
68
+ const { typeName } = currentComponent;
69
+ const propsType = propsTypes.get(typeName);
70
+ const used = usedProps.get(typeName);
71
+ if (propsType && used) {
72
+ Object.keys(propsType).forEach((prop) => {
73
+ if (!used.has(prop)) {
74
+ context.report({
75
+ node: propsType[prop],
76
+ messageId: 'unusedProp',
77
+ data: { propName: prop },
78
+ });
79
+ }
80
+ });
81
+ }
82
+ // Reset state for this component
83
+ propsTypes.delete(typeName);
84
+ usedProps.delete(typeName);
85
+ currentComponent = null;
86
+ }
87
+ },
88
+ };
89
+ },
90
+ });
91
+ //# sourceMappingURL=no-unused-props.js.map
@@ -0,0 +1,6 @@
1
+ import { TSESTree } from '@typescript-eslint/utils';
2
+ export declare const RULE_NAME = "require-dynamic-firebase-imports";
3
+ declare const _default: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"requireDynamicImport", never[], {
4
+ ImportDeclaration(node: TSESTree.ImportDeclaration): void;
5
+ }>;
6
+ export default _default;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RULE_NAME = void 0;
4
+ const createRule_1 = require("../utils/createRule");
5
+ exports.RULE_NAME = 'require-dynamic-firebase-imports';
6
+ exports.default = (0, createRule_1.createRule)({
7
+ name: exports.RULE_NAME,
8
+ meta: {
9
+ type: 'problem',
10
+ docs: {
11
+ description: 'Enforce dynamic imports for Firebase dependencies',
12
+ recommended: 'error',
13
+ },
14
+ fixable: 'code',
15
+ schema: [],
16
+ messages: {
17
+ requireDynamicImport: 'Firebase dependencies must be imported dynamically to reduce bundle size',
18
+ },
19
+ },
20
+ defaultOptions: [],
21
+ create(context) {
22
+ const isFirebaseImport = (source) => {
23
+ return (source.startsWith('firebase/') ||
24
+ source.includes('config/firebase-client'));
25
+ };
26
+ const createDynamicImport = (node) => {
27
+ const importSource = node.source.value;
28
+ const importSpecifiers = node.specifiers;
29
+ if (importSpecifiers.length === 0) {
30
+ // For side-effect imports like 'firebase/auth'
31
+ return `await import('${importSource}');`;
32
+ }
33
+ if (importSpecifiers.length === 1) {
34
+ const spec = importSpecifiers[0];
35
+ if (spec.type === 'ImportDefaultSpecifier') {
36
+ // For default imports
37
+ return `const ${spec.local.name} = (await import('${importSource}')).default;`;
38
+ }
39
+ if (spec.type === 'ImportSpecifier') {
40
+ // For single named import
41
+ const importedName = spec.imported.name;
42
+ const localName = spec.local.name;
43
+ if (importedName === localName) {
44
+ return `const { ${localName} } = await import('${importSource}');`;
45
+ }
46
+ return `const { ${importedName}: ${localName} } = await import('${importSource}');`;
47
+ }
48
+ }
49
+ // For multiple named imports
50
+ const importedModule = `await import('${importSource}')`;
51
+ const namedImports = importSpecifiers
52
+ .map((spec) => {
53
+ if (spec.type === 'ImportSpecifier') {
54
+ const importedName = spec.imported.name;
55
+ const localName = spec.local.name;
56
+ return importedName === localName
57
+ ? localName
58
+ : `${importedName}: ${localName}`;
59
+ }
60
+ return '';
61
+ })
62
+ .filter(Boolean)
63
+ .join(', ');
64
+ return `const { ${namedImports} } = ${importedModule};`;
65
+ };
66
+ return {
67
+ ImportDeclaration(node) {
68
+ const importSource = node.source.value;
69
+ if (typeof importSource === 'string' &&
70
+ isFirebaseImport(importSource)) {
71
+ context.report({
72
+ node,
73
+ messageId: 'requireDynamicImport',
74
+ fix(fixer) {
75
+ const dynamicImport = createDynamicImport(node);
76
+ return fixer.replaceText(node, dynamicImport);
77
+ },
78
+ });
79
+ }
80
+ },
81
+ };
82
+ },
83
+ });
84
+ //# sourceMappingURL=require-dynamic-firebase-imports.js.map
@@ -0,0 +1,6 @@
1
+ import { TSESTree } from '@typescript-eslint/utils';
2
+ declare const _default: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"useHttpsError" | "useProprietaryHttpsError", never[], {} | {
3
+ ImportDeclaration(node: TSESTree.ImportDeclaration): void;
4
+ ThrowStatement(node: TSESTree.ThrowStatement): void;
5
+ }>;
6
+ export = _default;
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ const utils_1 = require("@typescript-eslint/utils");
3
+ const createRule_1 = require("../utils/createRule");
4
+ module.exports = (0, createRule_1.createRule)({
5
+ name: 'require-https-error',
6
+ meta: {
7
+ type: 'problem',
8
+ docs: {
9
+ description: 'Enforce using proprietary HttpsError instead of throw new Error or firebase-admin HttpsError in functions/src',
10
+ recommended: 'error',
11
+ },
12
+ schema: [],
13
+ messages: {
14
+ useHttpsError: 'Use HttpsError instead of throw new Error in functions/src directory',
15
+ useProprietaryHttpsError: 'Use our proprietary HttpsError instead of firebase-admin HttpsError',
16
+ },
17
+ },
18
+ defaultOptions: [],
19
+ create(context) {
20
+ const filename = context.getFilename();
21
+ // Only apply rule to files in functions/src directory
22
+ if (!filename.includes('functions/src')) {
23
+ return {};
24
+ }
25
+ let hasFirebaseAdminImport = false;
26
+ let httpsIdentifier = null;
27
+ return {
28
+ ImportDeclaration(node) {
29
+ if (node.source.value === 'firebase-admin' ||
30
+ node.source.value === 'firebase-admin/lib/https-error') {
31
+ hasFirebaseAdminImport = true;
32
+ // Track the local name of the https import
33
+ const httpsSpecifier = node.specifiers.find((spec) => spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
34
+ spec.imported.name === 'https');
35
+ if (httpsSpecifier && 'local' in httpsSpecifier) {
36
+ httpsIdentifier = httpsSpecifier.local.name;
37
+ }
38
+ context.report({
39
+ node,
40
+ messageId: 'useProprietaryHttpsError',
41
+ });
42
+ }
43
+ },
44
+ ThrowStatement(node) {
45
+ const argument = node.argument;
46
+ if (argument &&
47
+ argument.type === utils_1.AST_NODE_TYPES.NewExpression &&
48
+ argument.callee) {
49
+ if (argument.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
50
+ argument.callee.name === 'Error') {
51
+ context.report({
52
+ node,
53
+ messageId: 'useHttpsError',
54
+ });
55
+ }
56
+ else if (hasFirebaseAdminImport &&
57
+ ((argument.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
58
+ argument.callee.name === 'HttpsError') ||
59
+ (argument.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
60
+ argument.callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
61
+ argument.callee.object.name === httpsIdentifier &&
62
+ argument.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
63
+ argument.callee.property.name === 'HttpsError'))) {
64
+ context.report({
65
+ node,
66
+ messageId: 'useProprietaryHttpsError',
67
+ });
68
+ }
69
+ }
70
+ },
71
+ };
72
+ },
73
+ });
74
+ //# sourceMappingURL=require-https-error.js.map
@@ -0,0 +1,7 @@
1
+ declare const _default: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"useImageOverlayed", {
2
+ componentPath: string;
3
+ }[], {
4
+ JSXElement(node: any): void;
5
+ ImportDeclaration(node: any): void;
6
+ }>;
7
+ export = _default;
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ const createRule_1 = require("../utils/createRule");
3
+ module.exports = (0, createRule_1.createRule)({
4
+ name: 'require-image-overlayed',
5
+ meta: {
6
+ type: 'problem',
7
+ docs: {
8
+ description: 'Enforce using ImageOverlayed component instead of next/image or img tags',
9
+ recommended: 'error',
10
+ },
11
+ fixable: 'code',
12
+ schema: [
13
+ {
14
+ type: 'object',
15
+ properties: {
16
+ componentPath: {
17
+ type: 'string',
18
+ default: 'src/components/ImageOverlayed',
19
+ },
20
+ },
21
+ additionalProperties: false,
22
+ },
23
+ ],
24
+ messages: {
25
+ useImageOverlayed: 'Use ImageOverlayed component from {{ componentPath }} instead of {{ component }}',
26
+ },
27
+ },
28
+ defaultOptions: [{ componentPath: 'src/components/ImageOverlayed' }],
29
+ create(context) {
30
+ const options = context.options[0] || {
31
+ componentPath: 'src/components/ImageOverlayed',
32
+ };
33
+ const sourceCode = context.getSourceCode();
34
+ return {
35
+ // Handle JSX img elements
36
+ JSXElement(node) {
37
+ if (node.openingElement.name.name === 'img') {
38
+ context.report({
39
+ node,
40
+ messageId: 'useImageOverlayed',
41
+ data: {
42
+ componentPath: options.componentPath,
43
+ component: 'img tag',
44
+ },
45
+ fix(fixer) {
46
+ const attributes = node.openingElement.attributes
47
+ .map((attr) => sourceCode.getText(attr))
48
+ .join(' ');
49
+ return fixer.replaceText(node, `<ImageOverlayed ${attributes} />`);
50
+ },
51
+ });
52
+ }
53
+ },
54
+ // Handle next/image imports and usage
55
+ ImportDeclaration(node) {
56
+ if (node.source.value === 'next/image' && node.specifiers.length > 0) {
57
+ const imageSpecifier = node.specifiers.find((spec) => (spec.type === 'ImportDefaultSpecifier' ||
58
+ spec.type === 'ImportSpecifier') &&
59
+ (spec.local.name === 'Image' || spec.imported?.name === 'Image'));
60
+ if (imageSpecifier) {
61
+ const localName = imageSpecifier.local.name;
62
+ // Report the import
63
+ context.report({
64
+ node,
65
+ messageId: 'useImageOverlayed',
66
+ data: {
67
+ componentPath: options.componentPath,
68
+ component: 'next/image',
69
+ },
70
+ fix(fixer) {
71
+ return fixer.replaceText(node, `import ${localName} from '${options.componentPath}';`);
72
+ },
73
+ });
74
+ }
75
+ }
76
+ },
77
+ };
78
+ },
79
+ });
80
+ //# sourceMappingURL=require-image-overlayed.js.map
@@ -45,7 +45,7 @@ const isUnmemoizedExportedFunctionComponent = (parentNode, node) => {
45
45
  };
46
46
  function isMemoImport(importPath) {
47
47
  // Match both absolute and relative paths ending with util/memo
48
- return /(?:^|\/)util\/memo$/.test(importPath);
48
+ return /(?:^|\/|\\)util\/memo$/.test(importPath);
49
49
  }
50
50
  function checkFunction(context, node) {
51
51
  const fileName = context.getFilename();
@@ -137,7 +137,7 @@ function calculateImportPath(currentFilePath) {
137
137
  // Calculate relative path based on current file depth from src
138
138
  // Subtract 1 from depth to exclude the filename itself
139
139
  const depth = parts.length - (srcIndex + 1) - 1;
140
- return '../'.repeat(depth) + 'util/memo';
140
+ return depth > 0 ? '../'.repeat(depth) + 'util/memo' : './util/memo';
141
141
  }
142
142
  exports.requireMemo = {
143
143
  create: (context) => ({
@@ -158,7 +158,7 @@ exports.requireMemo = {
158
158
  recommended: 'error',
159
159
  },
160
160
  messages: {
161
- requireMemo: 'Component definition not wrapped in React.memo()',
161
+ requireMemo: 'Component definition not wrapped in memo()',
162
162
  },
163
163
  schema: [],
164
164
  fixable: 'code',
@@ -0,0 +1,4 @@
1
+ import { TSESTree } from '@typescript-eslint/utils';
2
+ export declare const requireUseMemoObjectLiterals: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"requireUseMemo", never[], {
3
+ JSXAttribute(node: TSESTree.JSXAttribute): void;
4
+ }>;