@blumintinc/eslint-plugin-blumint 1.14.0 → 1.16.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 +36 -14
- package/lib/index.js +20 -12
- package/lib/rules/avoid-utils-directory.js +0 -4
- package/lib/rules/consistent-callback-naming.js +68 -3
- package/lib/rules/dynamic-https-errors.d.ts +1 -1
- package/lib/rules/dynamic-https-errors.js +119 -49
- package/lib/rules/enforce-boolean-naming-prefixes.d.ts +1 -0
- package/lib/rules/enforce-boolean-naming-prefixes.js +86 -331
- package/lib/rules/enforce-date-ttime.d.ts +1 -0
- package/lib/rules/enforce-date-ttime.js +156 -0
- package/lib/rules/enforce-dynamic-firebase-imports.d.ts +2 -1
- package/lib/rules/enforce-dynamic-firebase-imports.js +3 -2
- package/lib/rules/enforce-dynamic-imports.d.ts +2 -1
- package/lib/rules/enforce-dynamic-imports.js +42 -21
- package/lib/rules/enforce-f-extension-for-entry-points.d.ts +8 -0
- package/lib/rules/enforce-f-extension-for-entry-points.js +283 -0
- package/lib/rules/enforce-global-constants.js +3 -3
- package/lib/rules/enforce-id-capitalization.js +1 -1
- package/lib/rules/enforce-memoize-async.js +66 -15
- package/lib/rules/enforce-mui-rounded-icons.js +42 -1
- package/lib/rules/enforce-props-argument-name.js +42 -16
- package/lib/rules/enforce-stable-hash-spread-props.js +3 -3
- package/lib/rules/enforce-transform-memoization.js +1 -1
- package/lib/rules/enforce-verb-noun-naming.js +3817 -4641
- package/lib/rules/global-const-style.js +25 -4
- package/lib/rules/logical-top-to-bottom-grouping.js +80 -2
- package/lib/rules/memo-compare-deeply-complex-props.js +183 -6
- package/lib/rules/memo-nested-react-components.js +243 -103
- package/lib/rules/no-array-length-in-deps.js +74 -3
- package/lib/rules/no-async-foreach.js +7 -2
- package/lib/rules/no-circular-references.d.ts +2 -1
- package/lib/rules/no-circular-references.js +150 -489
- package/lib/rules/no-compositing-layer-props.js +31 -0
- package/lib/rules/no-console-error.js +12 -10
- package/lib/rules/no-empty-dependency-use-callbacks.js +1 -1
- package/lib/rules/no-entire-object-hook-deps.js +147 -65
- package/lib/rules/no-excessive-parent-chain.js +3 -0
- package/lib/rules/no-explicit-return-type.js +6 -0
- package/lib/rules/no-hungarian.js +119 -24
- package/lib/rules/no-inline-component-prop.js +16 -7
- package/lib/rules/no-margin-properties.js +7 -38
- package/lib/rules/no-passthrough-getters.d.ts +2 -2
- package/lib/rules/no-passthrough-getters.js +83 -1
- package/lib/rules/no-redundant-this-params.js +50 -1
- package/lib/rules/no-unmemoized-memo-without-props.js +1 -1
- package/lib/rules/no-unnecessary-destructuring-rename.js +2 -5
- package/lib/rules/no-unnecessary-verb-suffix.js +79 -0
- package/lib/rules/no-unused-props.js +215 -37
- package/lib/rules/no-useless-fragment.js +10 -2
- package/lib/rules/parallelize-async-operations.js +117 -54
- package/lib/rules/prefer-nullish-coalescing-boolean-props.js +109 -4
- package/lib/rules/prefer-params-over-parent-id.js +1 -1
- package/lib/rules/prefer-settings-object.js +27 -10
- package/lib/rules/prefer-type-alias-over-typeof-constant.js +75 -4
- package/lib/rules/prefer-use-deep-compare-memo.js +8 -14
- package/lib/rules/prefer-usememo-over-useeffect-usestate.js +1 -1
- package/lib/rules/prevent-children-clobber.js +9 -5
- package/lib/rules/react-memoize-literals.js +218 -13
- package/lib/rules/require-https-error-cause.js +30 -11
- package/lib/rules/require-memo.js +17 -9
- package/lib/rules/require-migration-script-metadata.d.ts +9 -0
- package/lib/rules/require-migration-script-metadata.js +206 -0
- package/lib/rules/warn-https-error-message-user-friendly.d.ts +1 -0
- package/lib/rules/warn-https-error-message-user-friendly.js +239 -0
- package/lib/utils/ASTHelpers.d.ts +49 -1
- package/lib/utils/ASTHelpers.js +394 -112
- package/package.json +7 -6
- package/release-manifest.json +166 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.enforceDateTTime = void 0;
|
|
27
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
28
|
+
const ts = __importStar(require("typescript"));
|
|
29
|
+
const createRule_1 = require("../utils/createRule");
|
|
30
|
+
exports.enforceDateTTime = (0, createRule_1.createRule)({
|
|
31
|
+
name: 'enforce-date-ttime',
|
|
32
|
+
meta: {
|
|
33
|
+
type: 'suggestion',
|
|
34
|
+
docs: {
|
|
35
|
+
description: 'Enforce that any generic type parameter named TTime is explicitly set to Date in frontend code',
|
|
36
|
+
recommended: 'error',
|
|
37
|
+
},
|
|
38
|
+
fixable: 'code',
|
|
39
|
+
schema: [],
|
|
40
|
+
messages: {
|
|
41
|
+
enforceDateTTime: '{{typeName}} leaves TTime unspecified or not Date → Firestore Frontend Hooks convert Timestamp to Date on the client, so non-Date TTime misrepresents runtime values and forces defensive conversions → Set the TTime argument to Date explicitly (e.g., {{typeName}}<..., Date, ...>).',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
defaultOptions: [],
|
|
45
|
+
create(context) {
|
|
46
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
47
|
+
const parserServices = sourceCode?.parserServices ?? context.parserServices;
|
|
48
|
+
const checker = parserServices?.program?.getTypeChecker();
|
|
49
|
+
if (!parserServices || !checker) {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
const tTimeIndexCache = new WeakMap();
|
|
53
|
+
/**
|
|
54
|
+
* Finds the index of the TTime parameter in the given symbol's declarations.
|
|
55
|
+
*/
|
|
56
|
+
function findTTimeParameterIndex(symbol) {
|
|
57
|
+
if (tTimeIndexCache.has(symbol)) {
|
|
58
|
+
return tTimeIndexCache.get(symbol);
|
|
59
|
+
}
|
|
60
|
+
const declarations = symbol.getDeclarations();
|
|
61
|
+
if (!declarations) {
|
|
62
|
+
tTimeIndexCache.set(symbol, undefined);
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
for (const declaration of declarations) {
|
|
66
|
+
if (ts.isTypeAliasDeclaration(declaration) ||
|
|
67
|
+
ts.isInterfaceDeclaration(declaration) ||
|
|
68
|
+
ts.isClassDeclaration(declaration)) {
|
|
69
|
+
if (declaration.typeParameters) {
|
|
70
|
+
const index = declaration.typeParameters.findIndex((tp) => tp.name.text === 'TTime');
|
|
71
|
+
if (index !== -1) {
|
|
72
|
+
tTimeIndexCache.set(symbol, index);
|
|
73
|
+
return index;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
tTimeIndexCache.set(symbol, undefined);
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Checks if a type node is exactly "Date".
|
|
83
|
+
*/
|
|
84
|
+
function isExactDate(node) {
|
|
85
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
86
|
+
node.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
87
|
+
node.typeName.name === 'Date' &&
|
|
88
|
+
(!node.typeParameters || node.typeParameters.params.length === 0)) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
TSTypeReference(node) {
|
|
95
|
+
const typeNameNode = node.typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName
|
|
96
|
+
? node.typeName.right
|
|
97
|
+
: node.typeName;
|
|
98
|
+
if (typeNameNode.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
99
|
+
return;
|
|
100
|
+
// Skip the "Date" type itself
|
|
101
|
+
if (typeNameNode.name === 'Date')
|
|
102
|
+
return;
|
|
103
|
+
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
|
|
104
|
+
const symbol = checker.getSymbolAtLocation(tsNode.typeName);
|
|
105
|
+
if (!symbol)
|
|
106
|
+
return;
|
|
107
|
+
const resolvedSymbol = symbol.flags & ts.SymbolFlags.Alias
|
|
108
|
+
? checker.getAliasedSymbol(symbol)
|
|
109
|
+
: symbol;
|
|
110
|
+
const tTimeIndex = findTTimeParameterIndex(resolvedSymbol);
|
|
111
|
+
if (tTimeIndex === undefined)
|
|
112
|
+
return;
|
|
113
|
+
const typeArgs = node.typeParameters?.params || [];
|
|
114
|
+
const tTimeArg = typeArgs[tTimeIndex];
|
|
115
|
+
const typeName = sourceCode.getText(node.typeName);
|
|
116
|
+
if (!tTimeArg) {
|
|
117
|
+
// TTime is omitted
|
|
118
|
+
context.report({
|
|
119
|
+
node,
|
|
120
|
+
messageId: 'enforceDateTTime',
|
|
121
|
+
data: { typeName },
|
|
122
|
+
fix(fixer) {
|
|
123
|
+
if (node.typeParameters) {
|
|
124
|
+
// Already has type parameters, but not enough to cover TTime
|
|
125
|
+
const lastParam = node.typeParameters.params[node.typeParameters.params.length - 1];
|
|
126
|
+
// Check if we can just append ", Date"
|
|
127
|
+
// We can only do this safely if all parameters between existing ones and TTime have defaults.
|
|
128
|
+
// For simplicity and safety, we only fix if tTimeIndex is the next one or if it's already provided.
|
|
129
|
+
if (tTimeIndex === node.typeParameters.params.length) {
|
|
130
|
+
return fixer.insertTextAfter(lastParam, ', Date');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else if (tTimeIndex === 0) {
|
|
134
|
+
// No type parameters and TTime is the first one
|
|
135
|
+
return fixer.insertTextAfter(node.typeName, '<Date>');
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
else if (!isExactDate(tTimeArg)) {
|
|
142
|
+
// TTime is provided but is not exactly Date
|
|
143
|
+
context.report({
|
|
144
|
+
node: tTimeArg,
|
|
145
|
+
messageId: 'enforceDateTTime',
|
|
146
|
+
data: { typeName },
|
|
147
|
+
fix(fixer) {
|
|
148
|
+
return fixer.replaceText(tTimeArg, 'Date');
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
//# sourceMappingURL=enforce-date-ttime.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
-
|
|
2
|
+
declare const enforceFirebaseImports: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"noDynamicImport", never[], {
|
|
3
3
|
ImportDeclaration(node: TSESTree.ImportDeclaration): void;
|
|
4
4
|
}>;
|
|
5
|
+
export default enforceFirebaseImports;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.enforceFirebaseImports = void 0;
|
|
4
3
|
const createRule_1 = require("../utils/createRule");
|
|
5
|
-
|
|
4
|
+
const enforceFirebaseImports = (0, createRule_1.createRule)({
|
|
6
5
|
name: 'enforce-dynamic-firebase-imports',
|
|
7
6
|
meta: {
|
|
8
7
|
type: 'problem',
|
|
@@ -112,6 +111,7 @@ exports.enforceFirebaseImports = (0, createRule_1.createRule)({
|
|
|
112
111
|
suggest: [
|
|
113
112
|
{
|
|
114
113
|
messageId: 'noDynamicImport',
|
|
114
|
+
data: { importPath },
|
|
115
115
|
fix(fixer) {
|
|
116
116
|
const replacement = buildReplacement({
|
|
117
117
|
allowSideEffectFix: true,
|
|
@@ -127,4 +127,5 @@ exports.enforceFirebaseImports = (0, createRule_1.createRule)({
|
|
|
127
127
|
};
|
|
128
128
|
},
|
|
129
129
|
});
|
|
130
|
+
exports.default = enforceFirebaseImports;
|
|
130
131
|
//# sourceMappingURL=enforce-dynamic-firebase-imports.js.map
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export declare const RULE_NAME = "enforce-dynamic-imports";
|
|
2
2
|
type Options = [
|
|
3
3
|
{
|
|
4
|
-
|
|
4
|
+
ignoredLibraries?: string[];
|
|
5
5
|
allowImportType?: boolean;
|
|
6
6
|
}
|
|
7
7
|
];
|
|
8
|
+
export declare const DEFAULT_IGNORED_LIBRARIES: string[];
|
|
8
9
|
declare const _default: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"dynamicImportRequired", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
9
10
|
export default _default;
|
|
@@ -1,21 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.RULE_NAME = void 0;
|
|
3
|
+
exports.DEFAULT_IGNORED_LIBRARIES = exports.RULE_NAME = void 0;
|
|
4
4
|
const createRule_1 = require("../utils/createRule");
|
|
5
|
+
const minimatch_1 = require("minimatch");
|
|
5
6
|
exports.RULE_NAME = 'enforce-dynamic-imports';
|
|
7
|
+
exports.DEFAULT_IGNORED_LIBRARIES = [
|
|
8
|
+
'react',
|
|
9
|
+
'react/**',
|
|
10
|
+
'react-dom',
|
|
11
|
+
'react-dom/**',
|
|
12
|
+
'next',
|
|
13
|
+
'next/**',
|
|
14
|
+
'@mui/material',
|
|
15
|
+
'@mui/material/**',
|
|
16
|
+
'@mui/icons-material',
|
|
17
|
+
'@mui/icons-material/**',
|
|
18
|
+
'@emotion/**',
|
|
19
|
+
'clsx',
|
|
20
|
+
'tailwind-merge',
|
|
21
|
+
];
|
|
6
22
|
exports.default = (0, createRule_1.createRule)({
|
|
7
23
|
name: exports.RULE_NAME,
|
|
8
24
|
meta: {
|
|
9
25
|
type: 'suggestion',
|
|
10
26
|
docs: {
|
|
11
|
-
description: 'Enforce dynamic imports for
|
|
27
|
+
description: 'Enforce dynamic imports for external libraries by default to optimize bundle size, unless explicitly ignored',
|
|
12
28
|
recommended: 'error',
|
|
13
29
|
},
|
|
14
30
|
schema: [
|
|
15
31
|
{
|
|
16
32
|
type: 'object',
|
|
17
33
|
properties: {
|
|
18
|
-
|
|
34
|
+
ignoredLibraries: {
|
|
19
35
|
type: 'array',
|
|
20
36
|
items: {
|
|
21
37
|
type: 'string',
|
|
@@ -29,46 +45,51 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
29
45
|
},
|
|
30
46
|
],
|
|
31
47
|
messages: {
|
|
32
|
-
dynamicImportRequired: 'Static import from "{{source}}"
|
|
48
|
+
dynamicImportRequired: 'Static import from "{{source}}" loads the full package into the initial bundle. → This increases download size and delays first render, undermining our lazy‑loading pattern for external dependencies. → Use a dynamic import (e.g., useDynamic(() => import("{{source}}"))), add "{{source}}" to ignoredLibraries for intentional static usage, or use a type‑only import when you only need types.',
|
|
33
49
|
},
|
|
34
50
|
},
|
|
35
51
|
defaultOptions: [
|
|
36
52
|
{
|
|
37
|
-
|
|
53
|
+
ignoredLibraries: exports.DEFAULT_IGNORED_LIBRARIES,
|
|
38
54
|
allowImportType: true,
|
|
39
55
|
},
|
|
40
56
|
],
|
|
41
57
|
create(context, [options]) {
|
|
42
|
-
const {
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
const { ignoredLibraries = exports.DEFAULT_IGNORED_LIBRARIES, allowImportType = true, } = options;
|
|
59
|
+
const exactIgnored = new Set();
|
|
60
|
+
const globIgnored = [];
|
|
61
|
+
for (const lib of ignoredLibraries) {
|
|
62
|
+
const mm = new minimatch_1.Minimatch(lib);
|
|
63
|
+
if (mm.hasMagic()) {
|
|
64
|
+
globIgnored.push(mm);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
exactIgnored.add(lib);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const isIgnored = (source) => {
|
|
71
|
+
return (exactIgnored.has(source) ||
|
|
72
|
+
globIgnored.some((mm) => mm.match(source)));
|
|
73
|
+
};
|
|
74
|
+
const isExternal = (source) => {
|
|
75
|
+
// Treat npm-style specifiers (including numeric names like '3d-force-graph') as external; internal paths are excluded.
|
|
76
|
+
return /^[a-z0-9@]/i.test(source) && !source.startsWith('@/');
|
|
54
77
|
};
|
|
55
78
|
return {
|
|
56
79
|
ImportDeclaration(node) {
|
|
57
80
|
const importSource = node.source.value;
|
|
58
81
|
// Skip type-only imports if allowed
|
|
59
82
|
if (allowImportType) {
|
|
60
|
-
// Check if it's a type-only import declaration
|
|
61
83
|
if (node.importKind === 'type') {
|
|
62
84
|
return;
|
|
63
85
|
}
|
|
64
|
-
// Check if all specifiers are type imports
|
|
65
86
|
if (node.specifiers.length > 0 &&
|
|
66
87
|
node.specifiers.every((spec) => spec.type === 'ImportSpecifier' && spec.importKind === 'type')) {
|
|
67
88
|
return;
|
|
68
89
|
}
|
|
69
90
|
}
|
|
70
|
-
//
|
|
71
|
-
if (
|
|
91
|
+
// Only enforce for external libraries that are NOT ignored
|
|
92
|
+
if (isExternal(importSource) && !isIgnored(importSource)) {
|
|
72
93
|
context.report({
|
|
73
94
|
node,
|
|
74
95
|
messageId: 'dynamicImportRequired',
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
type EnforceFExtensionOptions = [
|
|
3
|
+
{
|
|
4
|
+
entryPoints?: string[];
|
|
5
|
+
}
|
|
6
|
+
];
|
|
7
|
+
export declare const enforceFExtensionForEntryPoints: TSESLint.RuleModule<"requireFExtension", EnforceFExtensionOptions, TSESLint.RuleListener>;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.enforceFExtensionForEntryPoints = void 0;
|
|
7
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
8
|
+
const createRule_1 = require("../utils/createRule");
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
11
|
+
const DEFAULT_ENTRY_POINTS = [
|
|
12
|
+
'onCall',
|
|
13
|
+
'onCallVaripotent',
|
|
14
|
+
'onRequest',
|
|
15
|
+
'onQueueTask',
|
|
16
|
+
'onWebhook',
|
|
17
|
+
'sequentialDocumentWritten',
|
|
18
|
+
'onDocumentWritten',
|
|
19
|
+
'onDocumentCreated',
|
|
20
|
+
'onDocumentDeleted',
|
|
21
|
+
'onDocumentUpdated',
|
|
22
|
+
'onSchedule',
|
|
23
|
+
'onValueWritten',
|
|
24
|
+
'onValueCreated',
|
|
25
|
+
'onValueUpdated',
|
|
26
|
+
'onValueDeleted',
|
|
27
|
+
'sequentialValueWritten',
|
|
28
|
+
'sequentialValueCreated',
|
|
29
|
+
'sequentialValueUpdated',
|
|
30
|
+
'sequentialValueDeleted',
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* Entry points must be defined at the top level because Firebase deployment
|
|
34
|
+
* discovers and registers them by evaluating the module scope. Nested definitions
|
|
35
|
+
* inside functions or classes won't be discovered during the deployment process.
|
|
36
|
+
*/
|
|
37
|
+
function isTopLevel(node) {
|
|
38
|
+
let current = node.parent;
|
|
39
|
+
while (current) {
|
|
40
|
+
if (current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
41
|
+
current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
42
|
+
current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
43
|
+
current.type === utils_1.AST_NODE_TYPES.ClassDeclaration ||
|
|
44
|
+
current.type === utils_1.AST_NODE_TYPES.ClassExpression ||
|
|
45
|
+
current.type === utils_1.AST_NODE_TYPES.MethodDefinition) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
current = current.parent;
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Checks if a node directly defines an entry point as a function or variable declaration.
|
|
54
|
+
* This exempts wrapper implementation files (e.g., functions/src/v2/) but intentionally
|
|
55
|
+
* does not exempt re-export files (e.g., `const myCall = onCall; export { myCall }`).
|
|
56
|
+
* Re-export files that also invoke entry points must still follow the .f.ts naming convention
|
|
57
|
+
* and will be flagged by the CallExpression handler.
|
|
58
|
+
*
|
|
59
|
+
* This function specifically checks AST node types FunctionDeclaration and
|
|
60
|
+
* VariableDeclaration but does not catch ExportSpecifiers used in re-exports.
|
|
61
|
+
*/
|
|
62
|
+
function isNodeDefiningEntryPoint(node, entryPoints) {
|
|
63
|
+
// Handle function declaration: export function onCall() {}
|
|
64
|
+
if (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration &&
|
|
65
|
+
node.id &&
|
|
66
|
+
entryPoints.has(node.id.name)) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
// Handle variable declaration: export const onCall = ...
|
|
70
|
+
if (node.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
71
|
+
return node.declarations.some((decl) => decl.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
72
|
+
entryPoints.has(decl.id.name));
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Obtains the scope for a node in an ESLint 9+ compatible way.
|
|
78
|
+
*
|
|
79
|
+
* Note: In ESLint 8, falls back to context.getScope() which returns the scope
|
|
80
|
+
* of the currently visited node—only call this from within visitor handlers
|
|
81
|
+
* for the target node.
|
|
82
|
+
*/
|
|
83
|
+
function getScopeForNode(context, node) {
|
|
84
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
85
|
+
if (isEslint9OrLater(sourceCode)) {
|
|
86
|
+
return getEslint9Scope(sourceCode, node);
|
|
87
|
+
}
|
|
88
|
+
return context.getScope();
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Checks if the current environment is ESLint 9 or later.
|
|
92
|
+
*/
|
|
93
|
+
function isEslint9OrLater(sourceCode) {
|
|
94
|
+
return typeof sourceCode.getScope === 'function';
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Gets the scope for a node using the ESLint 9+ SourceCode.getScope() method.
|
|
98
|
+
*/
|
|
99
|
+
function getEslint9Scope(sourceCode, node) {
|
|
100
|
+
return sourceCode.getScope(node);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Gets the import definition for a given name in the current scope.
|
|
104
|
+
*/
|
|
105
|
+
function getImportDef(context, calleeName, node) {
|
|
106
|
+
const scope = getScopeForNode(context, node);
|
|
107
|
+
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(scope, calleeName);
|
|
108
|
+
if (!variable) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
return variable.defs.find((def) => def.type === 'ImportBinding') ?? null;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Checks if the import comes from an allowed Firebase or internal wrapper source.
|
|
115
|
+
*/
|
|
116
|
+
function isFromAllowedSource(importDef) {
|
|
117
|
+
if (!importDef || !importDef.parent) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
const importDeclaration = importDef.parent;
|
|
121
|
+
if (importDeclaration.type !== utils_1.AST_NODE_TYPES.ImportDeclaration) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
const source = importDeclaration.source.value;
|
|
125
|
+
return (source.startsWith('firebase-functions') ||
|
|
126
|
+
/(^|\/)v2(\/|$)/.test(source) ||
|
|
127
|
+
/(^|\/)util\/webhook(\/|$)/.test(source));
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Helper function to extract name from import specifier.
|
|
131
|
+
*/
|
|
132
|
+
function getNameFromImportSpecifier(node) {
|
|
133
|
+
const { imported } = node;
|
|
134
|
+
if (imported.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
135
|
+
return imported.name;
|
|
136
|
+
}
|
|
137
|
+
// Handle Literal imports (string-named imports in ESLint 8+)
|
|
138
|
+
const importedNode = imported;
|
|
139
|
+
if (importedNode.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
140
|
+
typeof importedNode.value === 'string') {
|
|
141
|
+
return importedNode.value;
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Helper function to extract name from module path.
|
|
147
|
+
*/
|
|
148
|
+
function getNameFromModulePath(importDeclaration, entryPoints) {
|
|
149
|
+
const modulePath = importDeclaration.source.value;
|
|
150
|
+
// Extract the module name from the path.
|
|
151
|
+
// Example: '../../v2/https/onCall' -> 'onCall'
|
|
152
|
+
const match = modulePath.match(/[/\\]([^/\\]+)$/);
|
|
153
|
+
const segment = (match ? match[1] : modulePath).replace(/\.[jt]sx?$/, '');
|
|
154
|
+
return entryPoints.has(segment) ? segment : undefined;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Resolves the original name of the imported function, handling aliases and default imports.
|
|
158
|
+
*/
|
|
159
|
+
function getOriginalName(importDef, calleeName, entryPoints) {
|
|
160
|
+
if (!importDef || !importDef.node) {
|
|
161
|
+
return calleeName;
|
|
162
|
+
}
|
|
163
|
+
// Handle named imports: import { onCall as myCall } from ...
|
|
164
|
+
if (importDef.node.type === utils_1.AST_NODE_TYPES.ImportSpecifier) {
|
|
165
|
+
return getNameFromImportSpecifier(importDef.node) ?? calleeName;
|
|
166
|
+
}
|
|
167
|
+
// Handle default imports: import onCall from '../../v2/https/onCall'
|
|
168
|
+
// or namespace imports: import * as onCall from '../../v2/https/onCall'
|
|
169
|
+
if (importDef.node.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier ||
|
|
170
|
+
importDef.node.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
|
|
171
|
+
return (getNameFromModulePath(importDef.parent, entryPoints) ?? calleeName);
|
|
172
|
+
}
|
|
173
|
+
return calleeName;
|
|
174
|
+
}
|
|
175
|
+
exports.enforceFExtensionForEntryPoints = (0, createRule_1.createRule)({
|
|
176
|
+
name: 'enforce-f-extension-for-entry-points',
|
|
177
|
+
meta: {
|
|
178
|
+
type: 'problem',
|
|
179
|
+
docs: {
|
|
180
|
+
description: 'Enforce .f.ts extension for entry points',
|
|
181
|
+
recommended: 'error',
|
|
182
|
+
},
|
|
183
|
+
schema: [
|
|
184
|
+
{
|
|
185
|
+
type: 'object',
|
|
186
|
+
properties: {
|
|
187
|
+
entryPoints: {
|
|
188
|
+
type: 'array',
|
|
189
|
+
items: { type: 'string' },
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
additionalProperties: false,
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
messages: {
|
|
196
|
+
requireFExtension: 'File "{{fileName}}" contains a Firebase Cloud Function entry point "{{entryPoint}}" but lacks the ".f.ts" extension. ' +
|
|
197
|
+
'Entry points must use ".f.ts" to clearly mark the public function surface to avoid accidental export/name collisions, ' +
|
|
198
|
+
'unintended exposure of internal implementation during deployment, confusion during imports/tests, and maintenance bugs. ' +
|
|
199
|
+
'Rename this file to "{{suggestedName}}" to mark it as a public entry point.',
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
defaultOptions: [{ entryPoints: DEFAULT_ENTRY_POINTS }],
|
|
203
|
+
create(context, [options]) {
|
|
204
|
+
const filePath = context.filename ??
|
|
205
|
+
context.getFilename();
|
|
206
|
+
const fileName = path_1.default.basename(filePath);
|
|
207
|
+
const entryPoints = new Set(options.entryPoints?.length ? options.entryPoints : DEFAULT_ENTRY_POINTS);
|
|
208
|
+
// Only apply to files under functions/src/
|
|
209
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
210
|
+
if (!normalizedPath.includes('/functions/src/') &&
|
|
211
|
+
!normalizedPath.startsWith('functions/src/')) {
|
|
212
|
+
return {};
|
|
213
|
+
}
|
|
214
|
+
// Exclude test files and declaration files
|
|
215
|
+
if (/\.(test|spec)\.tsx?$|\.d\.ts$/.test(fileName)) {
|
|
216
|
+
return {};
|
|
217
|
+
}
|
|
218
|
+
// Only apply to .ts and .tsx files, but not files already using .f.ts or .f.tsx
|
|
219
|
+
const isTsOrTsx = /\.tsx?$/.test(fileName);
|
|
220
|
+
const isFExtension = /\.f\.tsx?$/.test(fileName);
|
|
221
|
+
if (!isTsOrTsx || isFExtension) {
|
|
222
|
+
return {};
|
|
223
|
+
}
|
|
224
|
+
let isDefiningEntryPoint = false;
|
|
225
|
+
let reported = false;
|
|
226
|
+
return {
|
|
227
|
+
ExportNamedDeclaration(node) {
|
|
228
|
+
if (isDefiningEntryPoint || !node.declaration) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (isNodeDefiningEntryPoint(node.declaration, entryPoints)) {
|
|
232
|
+
isDefiningEntryPoint = true;
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
ExportDefaultDeclaration(node) {
|
|
236
|
+
if (isDefiningEntryPoint) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (isNodeDefiningEntryPoint(node.declaration, entryPoints)) {
|
|
240
|
+
isDefiningEntryPoint = true;
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
CallExpression(node) {
|
|
244
|
+
if (reported || isDefiningEntryPoint) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
// We only care about simple identifier calls (e.g., onCall())
|
|
248
|
+
if (node.callee.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const calleeName = node.callee.name;
|
|
252
|
+
// Optimization: Early return if it's not a top-level call
|
|
253
|
+
if (!isTopLevel(node)) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
// Variable lookup logic and import source validation
|
|
257
|
+
const importDef = getImportDef(context, calleeName, node);
|
|
258
|
+
if (!importDef || !isFromAllowedSource(importDef)) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
// Entry point checking
|
|
262
|
+
const originalName = getOriginalName(importDef, calleeName, entryPoints);
|
|
263
|
+
if (!entryPoints.has(originalName)) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
reported = true;
|
|
267
|
+
const suggestedName = fileName.replace(/\.tsx?$/, '.f$&');
|
|
268
|
+
context.report({
|
|
269
|
+
node,
|
|
270
|
+
messageId: 'requireFExtension',
|
|
271
|
+
data: {
|
|
272
|
+
fileName,
|
|
273
|
+
entryPoint: calleeName === originalName
|
|
274
|
+
? calleeName
|
|
275
|
+
: `${calleeName} (${originalName})`,
|
|
276
|
+
suggestedName,
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
//# sourceMappingURL=enforce-f-extension-for-entry-points.js.map
|
|
@@ -9,14 +9,14 @@ exports.enforceGlobalConstants = (0, createRule_1.createRule)({
|
|
|
9
9
|
meta: {
|
|
10
10
|
type: 'suggestion',
|
|
11
11
|
docs: {
|
|
12
|
-
description: 'Enforce
|
|
12
|
+
description: 'Enforce global static constants for React components/hooks',
|
|
13
13
|
recommended: 'error',
|
|
14
14
|
},
|
|
15
15
|
fixable: 'code',
|
|
16
16
|
schema: [],
|
|
17
17
|
messages: {
|
|
18
|
-
useGlobalConstant: '
|
|
19
|
-
extractDefaultToGlobalConstant: '
|
|
18
|
+
useGlobalConstant: 'Object literal returned from useMemo with empty dependencies creates a new reference every render without providing memoization benefits → this wastes memory and misleads readers into thinking the value is computed → move the object to a module-level constant (e.g., const OPTIONS = { ... } as const;).',
|
|
19
|
+
extractDefaultToGlobalConstant: 'Inline default value in destructuring creates a new reference on every render → this causes unnecessary re-renders in child components due to unstable identity → extract the default to a module-level constant (e.g., const DEFAULT_OPTIONS = { ... } as const;).',
|
|
20
20
|
},
|
|
21
21
|
},
|
|
22
22
|
defaultOptions: [],
|
|
@@ -19,7 +19,7 @@ exports.enforceIdCapitalization = (0, createRule_1.createRule)({
|
|
|
19
19
|
fixable: 'code',
|
|
20
20
|
schema: [],
|
|
21
21
|
messages: {
|
|
22
|
-
enforceIdCapitalization: '
|
|
22
|
+
enforceIdCapitalization: 'Found lowercase "id" in user-facing text → "ID" (capitalized) is the standard abbreviation for "identifier" and improves readability in UI labels and messages → replace lowercase "id" with uppercase "ID" in displayed labels and messages.',
|
|
23
23
|
},
|
|
24
24
|
},
|
|
25
25
|
defaultOptions: [],
|