@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.
Files changed (49) hide show
  1. package/README.md +21 -0
  2. package/lib/index.js +64 -1
  3. package/lib/rules/enforce-boolean-naming-prefixes.js +30 -14
  4. package/lib/rules/enforce-cloud-function-id-length.d.ts +5 -0
  5. package/lib/rules/enforce-cloud-function-id-length.js +104 -0
  6. package/lib/rules/enforce-is-prefix-validators.d.ts +13 -0
  7. package/lib/rules/enforce-is-prefix-validators.js +304 -0
  8. package/lib/rules/enforce-m3-sentence-case.d.ts +12 -0
  9. package/lib/rules/enforce-m3-sentence-case.js +430 -0
  10. package/lib/rules/enforce-snapshot-state-narrowing.d.ts +10 -0
  11. package/lib/rules/enforce-snapshot-state-narrowing.js +281 -0
  12. package/lib/rules/enforce-types-directory-placement.d.ts +9 -0
  13. package/lib/rules/enforce-types-directory-placement.js +276 -0
  14. package/lib/rules/no-direct-function-state.d.ts +8 -0
  15. package/lib/rules/no-direct-function-state.js +285 -0
  16. package/lib/rules/no-fill-template-mutation.d.ts +1 -0
  17. package/lib/rules/no-fill-template-mutation.js +324 -0
  18. package/lib/rules/no-portal-inside-tooltip.d.ts +10 -0
  19. package/lib/rules/no-portal-inside-tooltip.js +219 -0
  20. package/lib/rules/no-redundant-boolean-callback-props.d.ts +11 -0
  21. package/lib/rules/no-redundant-boolean-callback-props.js +355 -0
  22. package/lib/rules/no-satisfies-in-frontend-bundle.d.ts +8 -0
  23. package/lib/rules/no-satisfies-in-frontend-bundle.js +126 -0
  24. package/lib/rules/no-single-dismiss-dialog-button.d.ts +7 -0
  25. package/lib/rules/no-single-dismiss-dialog-button.js +127 -0
  26. package/lib/rules/no-stablehash-react-nodes.d.ts +1 -0
  27. package/lib/rules/no-stablehash-react-nodes.js +325 -0
  28. package/lib/rules/parallelize-loop-awaits.d.ts +8 -0
  29. package/lib/rules/parallelize-loop-awaits.js +582 -0
  30. package/lib/rules/prefer-flat-transform-each-keys.d.ts +1 -0
  31. package/lib/rules/prefer-flat-transform-each-keys.js +228 -0
  32. package/lib/rules/prefer-getter-over-parameterless-method.d.ts +1 -0
  33. package/lib/rules/prefer-getter-over-parameterless-method.js +44 -0
  34. package/lib/rules/prefer-spread-over-reassembly.d.ts +5 -0
  35. package/lib/rules/prefer-spread-over-reassembly.js +401 -0
  36. package/lib/rules/prefer-sx-prop-over-system-props.d.ts +9 -0
  37. package/lib/rules/prefer-sx-prop-over-system-props.js +401 -0
  38. package/lib/rules/prefer-use-base62-id.d.ts +8 -0
  39. package/lib/rules/prefer-use-base62-id.js +483 -0
  40. package/lib/rules/prefer-use-theme.d.ts +1 -0
  41. package/lib/rules/prefer-use-theme.js +206 -0
  42. package/lib/rules/prefer-utility-function-own-file.d.ts +9 -0
  43. package/lib/rules/prefer-utility-function-own-file.js +505 -0
  44. package/lib/rules/require-props-composition.d.ts +10 -0
  45. package/lib/rules/require-props-composition.js +433 -0
  46. package/lib/rules/require-server-timestamp-for-firestore-dates.d.ts +9 -0
  47. package/lib/rules/require-server-timestamp-for-firestore-dates.js +313 -0
  48. package/package.json +1 -1
  49. package/release-manifest.json +190 -0
@@ -0,0 +1,401 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.preferSxPropOverSystemProps = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ /**
7
+ * The canonical set of MUI system props that are deprecated in favor of `sx`.
8
+ * These are all props that MUI resolves via its system/styled engine and will
9
+ * remove in the next major release. Deliberately excludes props that serve a
10
+ * dual role as real component API (e.g. `spacing`, `direction` on Stack,
11
+ * Grid breakpoint props).
12
+ */
13
+ const MUI_SYSTEM_PROPS = new Set([
14
+ // Spacing — margin
15
+ 'm',
16
+ 'mt',
17
+ 'mr',
18
+ 'mb',
19
+ 'ml',
20
+ 'mx',
21
+ 'my',
22
+ // Spacing — padding
23
+ 'p',
24
+ 'pt',
25
+ 'pr',
26
+ 'pb',
27
+ 'pl',
28
+ 'px',
29
+ 'py',
30
+ // Sizing
31
+ 'width',
32
+ 'height',
33
+ 'minWidth',
34
+ 'maxWidth',
35
+ 'minHeight',
36
+ 'maxHeight',
37
+ 'boxSizing',
38
+ // Display / overflow / visibility
39
+ 'display',
40
+ 'displayPrint',
41
+ 'overflow',
42
+ 'textOverflow',
43
+ 'visibility',
44
+ 'whiteSpace',
45
+ // Flexbox
46
+ 'flexDirection',
47
+ 'flexWrap',
48
+ 'justifyContent',
49
+ 'justifyItems',
50
+ 'justifySelf',
51
+ 'alignItems',
52
+ 'alignContent',
53
+ 'alignSelf',
54
+ 'order',
55
+ 'flex',
56
+ 'flexGrow',
57
+ 'flexShrink',
58
+ 'flexBasis',
59
+ // CSS Grid
60
+ 'gap',
61
+ 'rowGap',
62
+ 'columnGap',
63
+ 'gridColumn',
64
+ 'gridRow',
65
+ 'gridArea',
66
+ 'gridAutoFlow',
67
+ 'gridAutoColumns',
68
+ 'gridAutoRows',
69
+ 'gridTemplateColumns',
70
+ 'gridTemplateRows',
71
+ 'gridTemplateAreas',
72
+ // Positioning
73
+ 'position',
74
+ 'top',
75
+ 'right',
76
+ 'bottom',
77
+ 'left',
78
+ 'zIndex',
79
+ // Color / background
80
+ 'color',
81
+ 'bgcolor',
82
+ // Borders
83
+ 'border',
84
+ 'borderTop',
85
+ 'borderRight',
86
+ 'borderBottom',
87
+ 'borderLeft',
88
+ 'borderColor',
89
+ 'borderTopColor',
90
+ 'borderRightColor',
91
+ 'borderBottomColor',
92
+ 'borderLeftColor',
93
+ 'borderRadius',
94
+ // Shadows
95
+ 'boxShadow',
96
+ // Typography
97
+ 'typography',
98
+ 'fontFamily',
99
+ 'fontSize',
100
+ 'fontStyle',
101
+ 'fontWeight',
102
+ 'letterSpacing',
103
+ 'lineHeight',
104
+ 'textAlign',
105
+ 'textTransform',
106
+ ]);
107
+ /**
108
+ * Default MUI component names to check. The user can extend this via options.
109
+ */
110
+ const DEFAULT_MUI_COMPONENTS = new Set([
111
+ 'Box',
112
+ 'Stack',
113
+ 'Typography',
114
+ 'Grid',
115
+ 'Paper',
116
+ 'Container',
117
+ 'Card',
118
+ 'CardContent',
119
+ 'CardActions',
120
+ 'Button',
121
+ 'IconButton',
122
+ 'Chip',
123
+ 'Avatar',
124
+ 'Badge',
125
+ 'Divider',
126
+ 'List',
127
+ 'ListItem',
128
+ 'ListItemText',
129
+ 'ListItemIcon',
130
+ 'Menu',
131
+ 'MenuItem',
132
+ 'Drawer',
133
+ 'Dialog',
134
+ 'DialogTitle',
135
+ 'DialogContent',
136
+ 'DialogActions',
137
+ 'Tabs',
138
+ 'Tab',
139
+ 'AppBar',
140
+ 'Toolbar',
141
+ ]);
142
+ /**
143
+ * Props that must never be moved to `sx` because they are genuine component
144
+ * API props, not MUI system styling shorthands. `direction` and `spacing` are
145
+ * the most critical — they control Stack's layout via MUI internals.
146
+ */
147
+ const DEFAULT_ALLOWED_PROPS = new Set([
148
+ 'direction',
149
+ 'spacing',
150
+ 'container',
151
+ 'item',
152
+ 'xs',
153
+ 'sm',
154
+ 'md',
155
+ 'lg',
156
+ 'xl',
157
+ 'variant',
158
+ 'component',
159
+ 'ref',
160
+ 'key',
161
+ 'children',
162
+ 'id',
163
+ 'className',
164
+ 'style',
165
+ 'divider',
166
+ 'useFlexGap',
167
+ 'columns',
168
+ 'wrap',
169
+ 'rowSpacing',
170
+ 'columnSpacing',
171
+ 'zeroMinWidth',
172
+ 'offset',
173
+ 'size', // Grid size (MUI v6+)
174
+ ]);
175
+ /** Get the component name from a JSX opening element (handles namespaced like Mui.Box). */
176
+ function getComponentName(node) {
177
+ const { name } = node;
178
+ if (name.type === utils_1.AST_NODE_TYPES.JSXIdentifier) {
179
+ return name.name;
180
+ }
181
+ if (name.type === utils_1.AST_NODE_TYPES.JSXMemberExpression) {
182
+ if (name.property.type === utils_1.AST_NODE_TYPES.JSXIdentifier) {
183
+ return name.property.name;
184
+ }
185
+ }
186
+ return null;
187
+ }
188
+ /** True when the first character is uppercase (i.e. a React/MUI component). */
189
+ function isUpperCase(name) {
190
+ return (name.length > 0 &&
191
+ name[0] === name[0].toUpperCase() &&
192
+ name[0] !== name[0].toLowerCase());
193
+ }
194
+ /**
195
+ * Convert a string value to a single-quoted JS string literal.
196
+ * Used when building sx property values from JSX string attributes.
197
+ */
198
+ function toSingleQuoted(value) {
199
+ const str = String(value);
200
+ const escaped = str.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
201
+ return `'${escaped}'`;
202
+ }
203
+ /** Serialize a JSX attribute value node back to source text for use inside sx={{ ... }}. */
204
+ function attrValueToSxValue(attr, sourceCode) {
205
+ const { value } = attr;
206
+ if (value === null) {
207
+ // Boolean shorthand — value is `true`
208
+ return 'true';
209
+ }
210
+ if (value.type === utils_1.AST_NODE_TYPES.Literal) {
211
+ // String literal attribute: display="flex" → 'flex'
212
+ return toSingleQuoted(value.value);
213
+ }
214
+ if (value.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer) {
215
+ if (value.expression.type === utils_1.AST_NODE_TYPES.JSXEmptyExpression) {
216
+ return 'undefined';
217
+ }
218
+ // Numeric, expression, object, array, etc. — preserve raw source
219
+ return sourceCode.getText(value.expression);
220
+ }
221
+ return sourceCode.getText(value);
222
+ }
223
+ exports.preferSxPropOverSystemProps = (0, createRule_1.createRule)({
224
+ name: 'prefer-sx-prop-over-system-props',
225
+ meta: {
226
+ type: 'suggestion',
227
+ docs: {
228
+ description: 'Enforce using the MUI `sx` prop instead of deprecated system props (e.g. `mt`, `display`, `flexDirection`) on MUI components.',
229
+ recommended: 'error',
230
+ },
231
+ fixable: 'code',
232
+ schema: [
233
+ {
234
+ type: 'object',
235
+ properties: {
236
+ components: {
237
+ type: 'array',
238
+ items: { type: 'string' },
239
+ },
240
+ allowedProps: {
241
+ type: 'array',
242
+ items: { type: 'string' },
243
+ },
244
+ },
245
+ additionalProperties: false,
246
+ },
247
+ ],
248
+ messages: {
249
+ preferSxProp: 'MUI system prop "{{prop}}" is deprecated. Move it into the `sx` prop instead.',
250
+ },
251
+ },
252
+ defaultOptions: [{}],
253
+ create(context, [options]) {
254
+ const componentSet = options.components
255
+ ? new Set(options.components)
256
+ : DEFAULT_MUI_COMPONENTS;
257
+ const extraAllowed = options.allowedProps
258
+ ? new Set(options.allowedProps)
259
+ : new Set();
260
+ function isAllowedProp(name) {
261
+ if (DEFAULT_ALLOWED_PROPS.has(name))
262
+ return true;
263
+ if (extraAllowed.has(name))
264
+ return true;
265
+ if (/^on[A-Z]/.test(name))
266
+ return true;
267
+ if (name.startsWith('aria-') || name.startsWith('data-'))
268
+ return true;
269
+ return false;
270
+ }
271
+ function isSystemProp(name) {
272
+ return MUI_SYSTEM_PROPS.has(name) && !isAllowedProp(name);
273
+ }
274
+ return {
275
+ JSXOpeningElement(node) {
276
+ const componentName = getComponentName(node);
277
+ if (!componentName)
278
+ return;
279
+ if (!isUpperCase(componentName))
280
+ return;
281
+ if (!componentSet.has(componentName))
282
+ return;
283
+ const systemPropAttrs = [];
284
+ let sxAttr = null;
285
+ for (const attr of node.attributes) {
286
+ if (attr.type !== utils_1.AST_NODE_TYPES.JSXAttribute)
287
+ continue;
288
+ if (attr.name.type !== utils_1.AST_NODE_TYPES.JSXIdentifier)
289
+ continue;
290
+ const name = attr.name.name;
291
+ if (name === 'sx') {
292
+ sxAttr = attr;
293
+ }
294
+ else if (isSystemProp(name)) {
295
+ systemPropAttrs.push(attr);
296
+ }
297
+ }
298
+ if (systemPropAttrs.length === 0)
299
+ return;
300
+ const sourceCode = context.getSourceCode();
301
+ const fullSource = sourceCode.getText();
302
+ /**
303
+ * Remove an attribute AND the whitespace that precedes it so the
304
+ * output does not accumulate extra spaces between attributes.
305
+ */
306
+ function removeAttrWithLeadingSpace(fixer, attr) {
307
+ const range = attr.range ?? [0, 0];
308
+ const [start, end] = range;
309
+ // Walk backwards from the attribute start to consume any whitespace
310
+ let wsStart = start;
311
+ while (wsStart > 0 && /\s/.test(fullSource[wsStart - 1])) {
312
+ wsStart--;
313
+ }
314
+ return fixer.removeRange([wsStart, end]);
315
+ }
316
+ // Report each system prop. Only the first carries the fixer to avoid
317
+ // overlapping fix ranges on the same element.
318
+ systemPropAttrs.forEach((attr, index) => {
319
+ const propName = attr.name.type === utils_1.AST_NODE_TYPES.JSXIdentifier
320
+ ? attr.name.name
321
+ : '';
322
+ context.report({
323
+ node: attr,
324
+ messageId: 'preferSxProp',
325
+ data: { prop: propName },
326
+ fix: index === 0
327
+ ? (fixer) => {
328
+ const sc = sourceCode;
329
+ const newEntries = systemPropAttrs
330
+ .map((a) => {
331
+ const key = a.name.type === utils_1.AST_NODE_TYPES.JSXIdentifier
332
+ ? a.name.name
333
+ : sc.getText(a.name);
334
+ const val = attrValueToSxValue(a, sc);
335
+ return `${key}: ${val}`;
336
+ })
337
+ .join(', ');
338
+ const fixes = [];
339
+ if (sxAttr === null) {
340
+ // No existing sx — replace the first system prop with sx={{...}}
341
+ // and delete the rest (including their leading whitespace).
342
+ fixes.push(fixer.replaceText(systemPropAttrs[0], `sx={{ ${newEntries} }}`));
343
+ for (let i = 1; i < systemPropAttrs.length; i++) {
344
+ fixes.push(removeAttrWithLeadingSpace(fixer, systemPropAttrs[i]));
345
+ }
346
+ }
347
+ else if (sxAttr.value?.type ===
348
+ utils_1.AST_NODE_TYPES.JSXExpressionContainer &&
349
+ sxAttr.value.expression.type ===
350
+ utils_1.AST_NODE_TYPES.ObjectExpression) {
351
+ // Existing sx={{ ... }} — merge system props at the front.
352
+ const existingObj = sxAttr.value.expression;
353
+ const existingProps = existingObj.properties;
354
+ if (existingProps.length === 0) {
355
+ fixes.push(fixer.replaceText(existingObj, `{ ${newEntries} }`));
356
+ }
357
+ else {
358
+ fixes.push(fixer.insertTextBefore(existingProps[0], `${newEntries}, `));
359
+ }
360
+ for (const a of systemPropAttrs) {
361
+ fixes.push(removeAttrWithLeadingSpace(fixer, a));
362
+ }
363
+ }
364
+ else if (sxAttr.value?.type ===
365
+ utils_1.AST_NODE_TYPES.JSXExpressionContainer &&
366
+ sxAttr.value.expression.type ===
367
+ utils_1.AST_NODE_TYPES.ArrayExpression) {
368
+ // sx={[...]} — prepend a new object as the first array element.
369
+ const arr = sxAttr.value.expression;
370
+ if (arr.elements.length === 0) {
371
+ fixes.push(fixer.replaceText(arr, `[{ ${newEntries} }]`));
372
+ }
373
+ else {
374
+ fixes.push(fixer.insertTextBefore(arr.elements[0], `{ ${newEntries} }, `));
375
+ }
376
+ for (const a of systemPropAttrs) {
377
+ fixes.push(removeAttrWithLeadingSpace(fixer, a));
378
+ }
379
+ }
380
+ else if (sxAttr.value !== null) {
381
+ // sx is a variable/expression — spread it: sx={{ entries, ...expr }}
382
+ const innerExpr = sxAttr.value.type ===
383
+ utils_1.AST_NODE_TYPES.JSXExpressionContainer
384
+ ? sc.getText(sxAttr.value
385
+ .expression)
386
+ : sc.getText(sxAttr.value);
387
+ fixes.push(fixer.replaceText(sxAttr, `sx={{ ${newEntries}, ...${innerExpr} }}`));
388
+ for (const a of systemPropAttrs) {
389
+ fixes.push(removeAttrWithLeadingSpace(fixer, a));
390
+ }
391
+ }
392
+ return fixes;
393
+ }
394
+ : null,
395
+ });
396
+ });
397
+ },
398
+ };
399
+ },
400
+ });
401
+ //# sourceMappingURL=prefer-sx-prop-over-system-props.js.map
@@ -0,0 +1,8 @@
1
+ type Options = [
2
+ {
3
+ targetPaths?: string[];
4
+ }
5
+ ];
6
+ type MessageIds = 'preferUseBase62IdHook' | 'preferUseBase62IdTopLevel' | 'preferUseBase62IdUseMemo';
7
+ export declare const preferUseBase62Id: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<MessageIds, Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
8
+ export {};