@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.
- package/README.md +1 -0
- package/lib/index.js +44 -2
- package/lib/rules/consistent-callback-naming.d.ts +2 -0
- package/lib/rules/consistent-callback-naming.js +84 -0
- package/lib/rules/enforce-callable-types.d.ts +3 -0
- package/lib/rules/enforce-callable-types.js +97 -0
- package/lib/rules/enforce-callback-memo.d.ts +3 -0
- package/lib/rules/enforce-callback-memo.js +79 -0
- package/lib/rules/enforce-dynamic-firebase-imports.d.ts +4 -0
- package/lib/rules/enforce-dynamic-firebase-imports.js +49 -0
- package/lib/rules/enforce-identifiable-firestore-type.d.ts +3 -0
- package/lib/rules/enforce-identifiable-firestore-type.js +89 -0
- package/lib/rules/enforce-serializable-params.d.ts +9 -0
- package/lib/rules/enforce-serializable-params.js +127 -0
- package/lib/rules/global-const-style.d.ts +5 -0
- package/lib/rules/global-const-style.js +77 -0
- package/lib/rules/no-jsx-whitespace-literal.d.ts +1 -0
- package/lib/rules/no-jsx-whitespace-literal.js +34 -0
- package/lib/rules/no-unused-props.d.ts +6 -0
- package/lib/rules/no-unused-props.js +91 -0
- package/lib/rules/require-dynamic-firebase-imports.d.ts +6 -0
- package/lib/rules/require-dynamic-firebase-imports.js +84 -0
- package/lib/rules/require-https-error.d.ts +6 -0
- package/lib/rules/require-https-error.js +74 -0
- package/lib/rules/require-image-overlayed.d.ts +7 -0
- package/lib/rules/require-image-overlayed.js +80 -0
- package/lib/rules/require-memo.js +3 -3
- package/lib/rules/require-usememo-object-literals.d.ts +4 -0
- package/lib/rules/require-usememo-object-literals.js +53 -0
- package/lib/rules/use-custom-link.d.ts +1 -0
- package/lib/rules/use-custom-link.js +49 -0
- package/lib/rules/use-custom-router.d.ts +1 -0
- package/lib/rules/use-custom-router.js +65 -0
- package/lib/utils/ASTHelpers.js +2 -0
- package/lib/utils/graph/ClassGraphBuilder.js +4 -0
- package/lib/utils/graph/ClassGraphSorterReadability.js +8 -3
- 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,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 /(
|
|
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
|
|
161
|
+
requireMemo: 'Component definition not wrapped in memo()',
|
|
162
162
|
},
|
|
163
163
|
schema: [],
|
|
164
164
|
fixable: 'code',
|