@blumintinc/eslint-plugin-blumint 1.16.2 → 1.17.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 +21 -0
- package/lib/index.js +64 -1
- package/lib/rules/enforce-boolean-naming-prefixes.js +30 -14
- package/lib/rules/enforce-cloud-function-id-length.d.ts +5 -0
- package/lib/rules/enforce-cloud-function-id-length.js +104 -0
- package/lib/rules/enforce-is-prefix-validators.d.ts +13 -0
- package/lib/rules/enforce-is-prefix-validators.js +304 -0
- package/lib/rules/enforce-m3-sentence-case.d.ts +12 -0
- package/lib/rules/enforce-m3-sentence-case.js +430 -0
- package/lib/rules/enforce-snapshot-state-narrowing.d.ts +10 -0
- package/lib/rules/enforce-snapshot-state-narrowing.js +281 -0
- package/lib/rules/enforce-types-directory-placement.d.ts +9 -0
- package/lib/rules/enforce-types-directory-placement.js +276 -0
- package/lib/rules/no-direct-function-state.d.ts +8 -0
- package/lib/rules/no-direct-function-state.js +285 -0
- package/lib/rules/no-fill-template-mutation.d.ts +1 -0
- package/lib/rules/no-fill-template-mutation.js +324 -0
- package/lib/rules/no-portal-inside-tooltip.d.ts +10 -0
- package/lib/rules/no-portal-inside-tooltip.js +219 -0
- package/lib/rules/no-redundant-boolean-callback-props.d.ts +11 -0
- package/lib/rules/no-redundant-boolean-callback-props.js +355 -0
- package/lib/rules/no-satisfies-in-frontend-bundle.d.ts +8 -0
- package/lib/rules/no-satisfies-in-frontend-bundle.js +126 -0
- package/lib/rules/no-single-dismiss-dialog-button.d.ts +7 -0
- package/lib/rules/no-single-dismiss-dialog-button.js +127 -0
- package/lib/rules/no-stablehash-react-nodes.d.ts +1 -0
- package/lib/rules/no-stablehash-react-nodes.js +325 -0
- package/lib/rules/parallelize-loop-awaits.d.ts +8 -0
- package/lib/rules/parallelize-loop-awaits.js +582 -0
- package/lib/rules/prefer-flat-transform-each-keys.d.ts +1 -0
- package/lib/rules/prefer-flat-transform-each-keys.js +228 -0
- package/lib/rules/prefer-getter-over-parameterless-method.d.ts +1 -0
- package/lib/rules/prefer-getter-over-parameterless-method.js +44 -0
- package/lib/rules/prefer-spread-over-reassembly.d.ts +5 -0
- package/lib/rules/prefer-spread-over-reassembly.js +401 -0
- package/lib/rules/prefer-sx-prop-over-system-props.d.ts +9 -0
- package/lib/rules/prefer-sx-prop-over-system-props.js +401 -0
- package/lib/rules/prefer-use-base62-id.d.ts +8 -0
- package/lib/rules/prefer-use-base62-id.js +483 -0
- package/lib/rules/prefer-use-theme.d.ts +1 -0
- package/lib/rules/prefer-use-theme.js +206 -0
- package/lib/rules/prefer-utility-function-own-file.d.ts +9 -0
- package/lib/rules/prefer-utility-function-own-file.js +505 -0
- package/lib/rules/require-props-composition.d.ts +10 -0
- package/lib/rules/require-props-composition.js +433 -0
- package/lib/rules/require-server-timestamp-for-firestore-dates.d.ts +9 -0
- package/lib/rules/require-server-timestamp-for-firestore-dates.js +313 -0
- package/package.json +1 -1
- package/release-manifest.json +190 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requireServerTimestampForFirestoreDates = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const minimatch_1 = require("minimatch");
|
|
6
|
+
const createRule_1 = require("../utils/createRule");
|
|
7
|
+
const DEFAULT_FIRESTORE_TYPE_PATHS = ['functions/src/types/firestore/**'];
|
|
8
|
+
const DEFAULT_TARGET_PATHS = ['src/**'];
|
|
9
|
+
const TEST_FILE_PATTERNS = [
|
|
10
|
+
/\.test\.[jt]sx?$/,
|
|
11
|
+
/\.spec\.[jt]sx?$/,
|
|
12
|
+
/__tests__\//,
|
|
13
|
+
/__mocks__\//,
|
|
14
|
+
];
|
|
15
|
+
function isTestFile(filename) {
|
|
16
|
+
return TEST_FILE_PATTERNS.some((pattern) => pattern.test(filename));
|
|
17
|
+
}
|
|
18
|
+
function matchesAnyGlob(value, globs) {
|
|
19
|
+
return globs.some((glob) => {
|
|
20
|
+
const mm = new minimatch_1.Minimatch(glob, { matchBase: false });
|
|
21
|
+
return mm.match(value);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Checks whether the import source matches any of the configured Firestore
|
|
26
|
+
* type path globs. Handles both absolute paths like
|
|
27
|
+
* `functions/src/types/firestore/TokenMetadata` and relative paths like
|
|
28
|
+
* `../../functions/src/types/firestore/TokenMetadata` by normalising the
|
|
29
|
+
* path before testing.
|
|
30
|
+
*/
|
|
31
|
+
function isFirestoreTypePath(source, firestoreTypePaths) {
|
|
32
|
+
// Strip leading relative segments so relative paths are tested the same way
|
|
33
|
+
// as bare module specifiers.
|
|
34
|
+
const normalized = source.replace(/^(\.\.\/|\.\/)+/, '');
|
|
35
|
+
return matchesAnyGlob(normalized, firestoreTypePaths);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Extracts the base type name (without generic parameters) from a
|
|
39
|
+
* TSTypeReference node. For `TokenMetadata<'offchain', Date>` this returns
|
|
40
|
+
* `TokenMetadata`.
|
|
41
|
+
*/
|
|
42
|
+
function getBaseTypeName(node) {
|
|
43
|
+
if (node.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
44
|
+
return node.typeName.name;
|
|
45
|
+
}
|
|
46
|
+
// Handle qualified names like `NS.TypeName`
|
|
47
|
+
if (node.typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
|
|
48
|
+
return node.typeName.right.name;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Recursively collects all TSTypeReference base names from a type node,
|
|
54
|
+
* including type parameters. This lets us detect Firestore types nested
|
|
55
|
+
* inside utility types like `Partial<TokenMetadata<...>>`.
|
|
56
|
+
*/
|
|
57
|
+
function collectTypeReferenceNames(typeNode) {
|
|
58
|
+
if (!typeNode)
|
|
59
|
+
return [];
|
|
60
|
+
const names = [];
|
|
61
|
+
if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
|
|
62
|
+
const base = getBaseTypeName(typeNode);
|
|
63
|
+
if (base)
|
|
64
|
+
names.push(base);
|
|
65
|
+
if (typeNode.typeParameters) {
|
|
66
|
+
for (const param of typeNode.typeParameters.params) {
|
|
67
|
+
names.push(...collectTypeReferenceNames(param));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else if (typeNode.type === utils_1.AST_NODE_TYPES.TSArrayType) {
|
|
72
|
+
names.push(...collectTypeReferenceNames(typeNode.elementType));
|
|
73
|
+
}
|
|
74
|
+
else if (typeNode.type === utils_1.AST_NODE_TYPES.TSUnionType ||
|
|
75
|
+
typeNode.type === utils_1.AST_NODE_TYPES.TSIntersectionType) {
|
|
76
|
+
for (const t of typeNode.types) {
|
|
77
|
+
names.push(...collectTypeReferenceNames(t));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeOperator) {
|
|
81
|
+
names.push(...collectTypeReferenceNames(typeNode.typeAnnotation));
|
|
82
|
+
}
|
|
83
|
+
return names;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Determines whether a type annotation references any of the tracked Firestore
|
|
87
|
+
* type names. The check is purely syntactic — we look for type names in
|
|
88
|
+
* TSTypeReference nodes.
|
|
89
|
+
*/
|
|
90
|
+
function typeAnnotationReferencesFirestoreType(typeAnnotation, firestoreTypeNames) {
|
|
91
|
+
const names = collectTypeReferenceNames(typeAnnotation);
|
|
92
|
+
return names.some((n) => firestoreTypeNames.has(n));
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Returns the inner expression, unwrapping TSAsExpression / TSSatisfiesExpression
|
|
96
|
+
* chains, so we can inspect what lies under a cast.
|
|
97
|
+
*/
|
|
98
|
+
function unwrapCast(node) {
|
|
99
|
+
let current = node;
|
|
100
|
+
while (current.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
101
|
+
current.type ===
|
|
102
|
+
utils_1.AST_NODE_TYPES
|
|
103
|
+
.TSSatisfiesExpression) {
|
|
104
|
+
current = current.expression;
|
|
105
|
+
}
|
|
106
|
+
return current;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Checks whether an expression is `new Date(...)` (possibly wrapped in casts).
|
|
110
|
+
*/
|
|
111
|
+
function isNewDate(node) {
|
|
112
|
+
const unwrapped = unwrapCast(node);
|
|
113
|
+
if (unwrapped.type !== utils_1.AST_NODE_TYPES.NewExpression)
|
|
114
|
+
return false;
|
|
115
|
+
const callee = unwrapped.callee;
|
|
116
|
+
return callee.type === utils_1.AST_NODE_TYPES.Identifier && callee.name === 'Date';
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Recursively visits all property values in an ObjectExpression (including
|
|
120
|
+
* nested objects) and reports any `new Date()` calls.
|
|
121
|
+
*/
|
|
122
|
+
function reportNewDatesInObject(obj,
|
|
123
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
124
|
+
context) {
|
|
125
|
+
for (const prop of obj.properties) {
|
|
126
|
+
if (prop.type !== utils_1.AST_NODE_TYPES.Property)
|
|
127
|
+
continue;
|
|
128
|
+
const value = prop.value;
|
|
129
|
+
if (isNewDate(value)) {
|
|
130
|
+
context.report({
|
|
131
|
+
node: value,
|
|
132
|
+
messageId: 'useServerTimestamp',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
else if (value.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
136
|
+
reportNewDatesInObject(value, context);
|
|
137
|
+
}
|
|
138
|
+
else if (value.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
139
|
+
value.type ===
|
|
140
|
+
utils_1.AST_NODE_TYPES
|
|
141
|
+
.TSSatisfiesExpression) {
|
|
142
|
+
// The cast may wrap a new Date() OR an object literal we need to recurse into
|
|
143
|
+
const inner = unwrapCast(value);
|
|
144
|
+
if (isNewDate(inner)) {
|
|
145
|
+
context.report({
|
|
146
|
+
node: value,
|
|
147
|
+
messageId: 'useServerTimestamp',
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
else if (inner.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
151
|
+
reportNewDatesInObject(inner, context);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else if (value.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
|
|
155
|
+
// Ternary: check both branches
|
|
156
|
+
if (isNewDate(value.consequent)) {
|
|
157
|
+
context.report({
|
|
158
|
+
node: value.consequent,
|
|
159
|
+
messageId: 'useServerTimestamp',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (isNewDate(value.alternate)) {
|
|
163
|
+
context.report({
|
|
164
|
+
node: value.alternate,
|
|
165
|
+
messageId: 'useServerTimestamp',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
exports.requireServerTimestampForFirestoreDates = (0, createRule_1.createRule)({
|
|
172
|
+
name: 'require-server-timestamp-for-firestore-dates',
|
|
173
|
+
meta: {
|
|
174
|
+
type: 'problem',
|
|
175
|
+
docs: {
|
|
176
|
+
description: 'Enforce serverTimestamp() instead of new Date() for Firestore timestamp fields',
|
|
177
|
+
recommended: 'error',
|
|
178
|
+
},
|
|
179
|
+
schema: [
|
|
180
|
+
{
|
|
181
|
+
type: 'object',
|
|
182
|
+
properties: {
|
|
183
|
+
firestoreTypePaths: {
|
|
184
|
+
type: 'array',
|
|
185
|
+
items: { type: 'string' },
|
|
186
|
+
},
|
|
187
|
+
targetPaths: {
|
|
188
|
+
type: 'array',
|
|
189
|
+
items: { type: 'string' },
|
|
190
|
+
},
|
|
191
|
+
ignoreTestFiles: {
|
|
192
|
+
type: 'boolean',
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
additionalProperties: false,
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
messages: {
|
|
199
|
+
useServerTimestamp: 'Use serverTimestamp() instead of new Date() for Firestore timestamp fields — client clocks are unreliable and spoofable.',
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
defaultOptions: [{}],
|
|
203
|
+
create(context, [options]) {
|
|
204
|
+
const firestoreTypePaths = options.firestoreTypePaths ?? DEFAULT_FIRESTORE_TYPE_PATHS;
|
|
205
|
+
const targetPaths = options.targetPaths ?? DEFAULT_TARGET_PATHS;
|
|
206
|
+
const ignoreTestFiles = options.ignoreTestFiles ?? true;
|
|
207
|
+
const filename = context.getFilename();
|
|
208
|
+
// Honour ignoreTestFiles option
|
|
209
|
+
if (ignoreTestFiles && isTestFile(filename)) {
|
|
210
|
+
return {};
|
|
211
|
+
}
|
|
212
|
+
// Only apply rule to target paths (frontend code by default).
|
|
213
|
+
// Normalise to forward slashes so Windows paths work. Test filenames are
|
|
214
|
+
// relative (e.g. `src/hooks/useExample.ts`). Real monorepo filenames are
|
|
215
|
+
// absolute (e.g. `/workspace/src/hooks/useExample.ts`); for those we also
|
|
216
|
+
// try matching the segment of the path that starts with the first literal
|
|
217
|
+
// prefix of each glob (only when the filename is an absolute path).
|
|
218
|
+
const normalizedFilename = filename.replace(/\\/g, '/');
|
|
219
|
+
const isAbsolutePath = normalizedFilename.startsWith('/');
|
|
220
|
+
const inTargetPaths = matchesAnyGlob(normalizedFilename, targetPaths) ||
|
|
221
|
+
(isAbsolutePath &&
|
|
222
|
+
targetPaths.some((glob) => {
|
|
223
|
+
const prefix = glob.replace(/\*.*$/, ''); // e.g. 'src/'
|
|
224
|
+
const idx = normalizedFilename.indexOf('/' + prefix);
|
|
225
|
+
if (idx === -1)
|
|
226
|
+
return false;
|
|
227
|
+
const suffix = normalizedFilename.slice(idx + 1); // e.g. 'src/hooks/...'
|
|
228
|
+
return matchesAnyGlob(suffix, [glob]);
|
|
229
|
+
}));
|
|
230
|
+
if (!inTargetPaths)
|
|
231
|
+
return {};
|
|
232
|
+
// Collect Firestore type names from imports in this file
|
|
233
|
+
const firestoreTypeNames = new Set();
|
|
234
|
+
return {
|
|
235
|
+
ImportDeclaration(node) {
|
|
236
|
+
const source = node.source.value;
|
|
237
|
+
if (!isFirestoreTypePath(source, firestoreTypePaths))
|
|
238
|
+
return;
|
|
239
|
+
for (const specifier of node.specifiers) {
|
|
240
|
+
if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier ||
|
|
241
|
+
specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier ||
|
|
242
|
+
specifier.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
|
|
243
|
+
// Track the local alias name (what this file uses to refer to the type)
|
|
244
|
+
firestoreTypeNames.add(specifier.local.name);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
VariableDeclarator(node) {
|
|
249
|
+
if (firestoreTypeNames.size === 0)
|
|
250
|
+
return;
|
|
251
|
+
// Pattern: const x: FirestoreType = { ... }
|
|
252
|
+
const typeAnnotation = node.id.typeAnnotation?.typeAnnotation;
|
|
253
|
+
if (typeAnnotation &&
|
|
254
|
+
typeAnnotationReferencesFirestoreType(typeAnnotation, firestoreTypeNames) &&
|
|
255
|
+
node.init &&
|
|
256
|
+
node.init.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
257
|
+
reportNewDatesInObject(node.init, context);
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
// Pattern: { ... } as FirestoreType or { ... } satisfies FirestoreType
|
|
261
|
+
TSAsExpression(node) {
|
|
262
|
+
if (firestoreTypeNames.size === 0)
|
|
263
|
+
return;
|
|
264
|
+
// Only check when the as-cast target type references a Firestore type
|
|
265
|
+
const typeAnnotation = node.typeAnnotation;
|
|
266
|
+
if (!typeAnnotationReferencesFirestoreType(typeAnnotation, firestoreTypeNames))
|
|
267
|
+
return;
|
|
268
|
+
const inner = unwrapCast(node.expression);
|
|
269
|
+
if (inner.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
270
|
+
reportNewDatesInObject(inner, context);
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
// Arrow functions with concise body: `const f = (): FirestoreType => ({ ... })`
|
|
274
|
+
// In this form the body is the ObjectExpression directly (no ReturnStatement).
|
|
275
|
+
ArrowFunctionExpression(node) {
|
|
276
|
+
if (firestoreTypeNames.size === 0)
|
|
277
|
+
return;
|
|
278
|
+
if (node.body.type !== utils_1.AST_NODE_TYPES.ObjectExpression)
|
|
279
|
+
return;
|
|
280
|
+
const returnType = node.returnType?.typeAnnotation;
|
|
281
|
+
if (returnType &&
|
|
282
|
+
typeAnnotationReferencesFirestoreType(returnType, firestoreTypeNames)) {
|
|
283
|
+
reportNewDatesInObject(node.body, context);
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
// Return statements in functions with explicit Firestore return type annotation
|
|
287
|
+
ReturnStatement(node) {
|
|
288
|
+
if (firestoreTypeNames.size === 0)
|
|
289
|
+
return;
|
|
290
|
+
if (!node.argument)
|
|
291
|
+
return;
|
|
292
|
+
if (node.argument.type !== utils_1.AST_NODE_TYPES.ObjectExpression)
|
|
293
|
+
return;
|
|
294
|
+
// Walk up to find the enclosing function and check its return type
|
|
295
|
+
let ancestor = node.parent;
|
|
296
|
+
while (ancestor) {
|
|
297
|
+
if (ancestor.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
298
|
+
ancestor.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
299
|
+
ancestor.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
300
|
+
const returnType = ancestor.returnType?.typeAnnotation;
|
|
301
|
+
if (returnType &&
|
|
302
|
+
typeAnnotationReferencesFirestoreType(returnType, firestoreTypeNames)) {
|
|
303
|
+
reportNewDatesInObject(node.argument, context);
|
|
304
|
+
}
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
ancestor = ancestor.parent;
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
//# sourceMappingURL=require-server-timestamp-for-firestore-dates.js.map
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,194 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.17.0",
|
|
4
|
+
"date": "2026-07-01T05:16:18.225Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-boolean-naming-prefixes",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1249
|
|
11
|
+
],
|
|
12
|
+
"summary": "require boolean prefix at a name boundary in callExpressionLooksBoolean (closes #1249)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "enforce-cloud-function-id-length",
|
|
16
|
+
"changeType": "feat",
|
|
17
|
+
"issues": [
|
|
18
|
+
1222
|
|
19
|
+
],
|
|
20
|
+
"summary": "flag .f.ts paths deriving Firebase IDs over 62 chars (closes #1222)"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "enforce-is-prefix-validators",
|
|
24
|
+
"changeType": "feat",
|
|
25
|
+
"issues": [
|
|
26
|
+
1193
|
|
27
|
+
],
|
|
28
|
+
"summary": "require is-prefix on exported validators (closes #1193)"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "enforce-m3-sentence-case",
|
|
32
|
+
"changeType": "feat",
|
|
33
|
+
"issues": [
|
|
34
|
+
1190
|
|
35
|
+
],
|
|
36
|
+
"summary": "warn on Title Case / ALL CAPS user-facing text (closes #1190)"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "enforce-snapshot-state-narrowing",
|
|
40
|
+
"changeType": "feat",
|
|
41
|
+
"issues": [
|
|
42
|
+
1210
|
|
43
|
+
],
|
|
44
|
+
"summary": "require isSnapshotReady over falsy/typeof checks (closes #1210)"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "enforce-types-directory-placement",
|
|
48
|
+
"changeType": "feat",
|
|
49
|
+
"issues": [
|
|
50
|
+
1194
|
|
51
|
+
],
|
|
52
|
+
"summary": "flag type-only files outside functions/src/types (closes #1194)"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "no-direct-function-state",
|
|
56
|
+
"changeType": "feat",
|
|
57
|
+
"issues": [
|
|
58
|
+
1208
|
|
59
|
+
],
|
|
60
|
+
"summary": "flag functions passed directly to useState setters (closes #1208)"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "no-fill-template-mutation",
|
|
64
|
+
"changeType": "feat",
|
|
65
|
+
"issues": [
|
|
66
|
+
1209
|
|
67
|
+
],
|
|
68
|
+
"summary": "forbid mutating fillTemplate() results (closes #1209)"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"name": "no-portal-inside-tooltip",
|
|
72
|
+
"changeType": "feat",
|
|
73
|
+
"issues": [
|
|
74
|
+
1223
|
|
75
|
+
],
|
|
76
|
+
"summary": "flag portals nested in tooltip wrappers (closes #1223)"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"name": "no-redundant-boolean-callback-props",
|
|
80
|
+
"changeType": "feat",
|
|
81
|
+
"issues": [
|
|
82
|
+
1192
|
|
83
|
+
],
|
|
84
|
+
"summary": "flag boolean props redundant with a callback's presence (closes #1192)"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"name": "no-satisfies-in-frontend-bundle",
|
|
88
|
+
"changeType": "feat",
|
|
89
|
+
"issues": [
|
|
90
|
+
1226
|
|
91
|
+
],
|
|
92
|
+
"summary": "ban satisfies in webpack-bundled files (closes #1226)"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"name": "no-single-dismiss-dialog-button",
|
|
96
|
+
"changeType": "feat",
|
|
97
|
+
"issues": [
|
|
98
|
+
1221
|
|
99
|
+
],
|
|
100
|
+
"summary": "flag lone dismiss button in dialog buttons array (closes #1221)"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"name": "no-stablehash-react-nodes",
|
|
104
|
+
"changeType": "feat",
|
|
105
|
+
"issues": [
|
|
106
|
+
1134
|
|
107
|
+
],
|
|
108
|
+
"summary": "flag stableHash() on ReactNodes/KeyedNodes (closes #1134)"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"name": "parallelize-loop-awaits",
|
|
112
|
+
"changeType": "feat",
|
|
113
|
+
"issues": [
|
|
114
|
+
1184
|
|
115
|
+
],
|
|
116
|
+
"summary": "flag sequential await in loops parallelizable via Promise.all (closes #1184)"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"name": "prefer-flat-transform-each-keys",
|
|
120
|
+
"changeType": "feat",
|
|
121
|
+
"issues": [
|
|
122
|
+
1212
|
|
123
|
+
],
|
|
124
|
+
"summary": "flag nested objects in propagation transformEach returns (closes #1212)"
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"name": "prefer-getter-over-parameterless-method",
|
|
128
|
+
"changeType": "fix",
|
|
129
|
+
"issues": [
|
|
130
|
+
1248
|
|
131
|
+
],
|
|
132
|
+
"summary": "exempt throwing and builder/factory methods (closes #1248)"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"name": "prefer-spread-over-reassembly",
|
|
136
|
+
"changeType": "feat",
|
|
137
|
+
"issues": [
|
|
138
|
+
1188
|
|
139
|
+
],
|
|
140
|
+
"summary": "flag destructure-then-reassemble prop forwarding (closes #1188)"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"name": "prefer-sx-prop-over-system-props",
|
|
144
|
+
"changeType": "feat",
|
|
145
|
+
"issues": [
|
|
146
|
+
1189
|
|
147
|
+
],
|
|
148
|
+
"summary": "migrate deprecated MUI system props into sx (closes #1189)"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"name": "prefer-use-base62-id",
|
|
152
|
+
"changeType": "feat",
|
|
153
|
+
"issues": [
|
|
154
|
+
1206
|
|
155
|
+
],
|
|
156
|
+
"summary": "prefer useBase62Id() over useState/useRef/useMemo + uuidv4Base62() (closes #1206)"
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"name": "prefer-use-theme",
|
|
160
|
+
"changeType": "feat",
|
|
161
|
+
"issues": [
|
|
162
|
+
1213
|
|
163
|
+
],
|
|
164
|
+
"summary": "flag direct theme-constant imports over useTheme() (closes #1213)"
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"name": "prefer-utility-function-own-file",
|
|
168
|
+
"changeType": "feat",
|
|
169
|
+
"issues": [
|
|
170
|
+
1234
|
|
171
|
+
],
|
|
172
|
+
"summary": "flag sizable co-located utility functions (closes #1234)"
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"name": "require-props-composition",
|
|
176
|
+
"changeType": "feat",
|
|
177
|
+
"issues": [
|
|
178
|
+
1181
|
|
179
|
+
],
|
|
180
|
+
"summary": "flag component Props that don't compose with rendered children (closes #1181)"
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"name": "require-server-timestamp-for-firestore-dates",
|
|
184
|
+
"changeType": "feat",
|
|
185
|
+
"issues": [
|
|
186
|
+
1183
|
|
187
|
+
],
|
|
188
|
+
"summary": "flag new Date() in Firestore-typed objects (closes #1183)"
|
|
189
|
+
}
|
|
190
|
+
]
|
|
191
|
+
},
|
|
2
192
|
{
|
|
3
193
|
"version": "1.16.2",
|
|
4
194
|
"date": "2026-06-30T22:17:37.457Z",
|