@blumintinc/eslint-plugin-blumint 1.15.0 → 1.16.1

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 (45) hide show
  1. package/README.md +28 -5
  2. package/lib/index.js +7 -1
  3. package/lib/rules/consistent-callback-naming.js +26 -0
  4. package/lib/rules/dynamic-https-errors.js +5 -26
  5. package/lib/rules/enforce-assert-safe-object-key.js +29 -0
  6. package/lib/rules/enforce-boolean-naming-prefixes.d.ts +1 -0
  7. package/lib/rules/enforce-boolean-naming-prefixes.js +86 -119
  8. package/lib/rules/enforce-dynamic-imports.d.ts +5 -1
  9. package/lib/rules/enforce-dynamic-imports.js +90 -19
  10. package/lib/rules/enforce-memoize-async.js +1 -4
  11. package/lib/rules/enforce-mui-rounded-icons.js +42 -1
  12. package/lib/rules/enforce-verb-noun-naming.js +3 -0
  13. package/lib/rules/global-const-style.js +9 -0
  14. package/lib/rules/logical-top-to-bottom-grouping.js +80 -2
  15. package/lib/rules/memo-compare-deeply-complex-props.js +167 -3
  16. package/lib/rules/memo-nested-react-components.js +143 -8
  17. package/lib/rules/no-array-length-in-deps.js +74 -3
  18. package/lib/rules/no-circular-references.js +145 -482
  19. package/lib/rules/no-compositing-layer-props.js +31 -0
  20. package/lib/rules/no-entire-object-hook-deps.js +132 -97
  21. package/lib/rules/no-explicit-return-type.js +6 -0
  22. package/lib/rules/no-hungarian.js +119 -24
  23. package/lib/rules/no-margin-properties.js +7 -38
  24. package/lib/rules/no-unnecessary-verb-suffix.js +79 -0
  25. package/lib/rules/no-unused-props.js +215 -37
  26. package/lib/rules/no-useless-fragment.js +10 -2
  27. package/lib/rules/parallelize-async-operations.js +1 -3
  28. package/lib/rules/prefer-type-alias-over-typeof-constant.js +73 -11
  29. package/lib/rules/prefer-use-deep-compare-memo.js +8 -14
  30. package/lib/rules/react-memoize-literals.js +87 -1
  31. package/lib/rules/require-memo.js +8 -0
  32. package/lib/rules/require-migration-script-metadata.d.ts +9 -0
  33. package/lib/rules/require-migration-script-metadata.js +206 -0
  34. package/lib/rules/warn-https-error-message-user-friendly.d.ts +1 -0
  35. package/lib/rules/warn-https-error-message-user-friendly.js +239 -0
  36. package/lib/utils/ASTHelpers.d.ts +15 -0
  37. package/lib/utils/ASTHelpers.js +48 -0
  38. package/package.json +7 -6
  39. package/release-manifest.json +196 -0
  40. package/lib/rules/prefer-memoized-props.d.ts +0 -3
  41. package/lib/rules/prefer-memoized-props.js +0 -445
  42. package/lib/rules/prefer-nullish-coalescing-override.d.ts +0 -7
  43. package/lib/rules/prefer-nullish-coalescing-override.js +0 -188
  44. package/lib/rules/require-usememo-object-literals.d.ts +0 -4
  45. package/lib/rules/require-usememo-object-literals.js +0 -64
@@ -33,6 +33,14 @@ const DEFAULT_BOOLEAN_PREFIXES = [
33
33
  const DEFAULT_OPTIONS = {
34
34
  prefixes: DEFAULT_BOOLEAN_PREFIXES,
35
35
  ignoreOverriddenGetters: false,
36
+ // Property names declared in interfaces and type aliases are frequently
37
+ // dictated by contracts the author cannot rename: external API request/response
38
+ // shapes, third-party library interfaces, and persisted data-model schemas
39
+ // (e.g. Firestore document fields). Flagging those produces unavoidable false
40
+ // positives, so property-signature enforcement is opt-in. Names the author does
41
+ // choose freshly — variables, function returns, class fields, parameters — stay
42
+ // enforced by default.
43
+ enforceForPropertySignatures: false,
36
44
  };
37
45
  const BOOLEANISH_BINARY_OPERATORS = new Set(['===', '!==', '==', '!=', '>', '<', '>=', '<=', 'in', 'instanceof']);
38
46
  exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
@@ -58,6 +66,10 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
58
66
  type: 'boolean',
59
67
  default: false,
60
68
  },
69
+ enforceForPropertySignatures: {
70
+ type: 'boolean',
71
+ default: false,
72
+ },
61
73
  },
62
74
  additionalProperties: false,
63
75
  },
@@ -71,8 +83,11 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
71
83
  defaultOptions: [DEFAULT_OPTIONS],
72
84
  create(context, [options]) {
73
85
  const approvedPrefixes = options.prefixes || DEFAULT_OPTIONS.prefixes;
86
+ const approvedPrefixesWithoutAsserts = approvedPrefixes.filter((p) => p !== 'asserts');
74
87
  const ignoreOverriddenGetters = options.ignoreOverriddenGetters ??
75
88
  DEFAULT_OPTIONS.ignoreOverriddenGetters;
89
+ const enforceForPropertySignatures = options.enforceForPropertySignatures ??
90
+ DEFAULT_OPTIONS.enforceForPropertySignatures;
76
91
  function findVariableInScopes(name) {
77
92
  let currentScope = context.getScope();
78
93
  while (currentScope) {
@@ -83,32 +98,58 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
83
98
  }
84
99
  return undefined;
85
100
  }
101
+ /**
102
+ * Check if a name is prefixed by a boolean keyword with proper boundaries.
103
+ * Supports camelCase (isSomething), snake_case (is_something, IS_SOMETHING),
104
+ * and exact matches.
105
+ */
106
+ function isPrefixedByBooleanKeyword(name, prefixes) {
107
+ const normalizedName = name.startsWith('_') ? name.slice(1) : name;
108
+ const checkPrefix = (p) => {
109
+ if (!normalizedName.toLowerCase().startsWith(p.toLowerCase())) {
110
+ return false;
111
+ }
112
+ if (normalizedName.length === p.length) {
113
+ return true;
114
+ }
115
+ const nextChar = normalizedName.charAt(p.length);
116
+ // For SCREAMING_SNAKE_CASE or similar all-uppercase names,
117
+ // we require an underscore boundary.
118
+ const isAllUppercase = normalizedName === normalizedName.toUpperCase() &&
119
+ /[a-z]/i.test(normalizedName);
120
+ if (isAllUppercase) {
121
+ return nextChar === '_';
122
+ }
123
+ // For camelCase, the next char must be uppercase, a digit, or $
124
+ return (nextChar === '_' ||
125
+ nextChar === '$' ||
126
+ (nextChar >= '0' && nextChar <= '9') ||
127
+ (nextChar === nextChar.toUpperCase() &&
128
+ nextChar !== nextChar.toLowerCase()));
129
+ };
130
+ const checkPrefixWithPlural = (p) => {
131
+ if (checkPrefix(p))
132
+ return true;
133
+ if (['is', 'has', 'does', 'was', 'had', 'did'].includes(p)) {
134
+ const pluralPrefix = pluralize_1.default.plural(p);
135
+ if (checkPrefix(pluralPrefix))
136
+ return true;
137
+ }
138
+ return false;
139
+ };
140
+ return prefixes.some((prefix) => checkPrefixWithPlural(prefix));
141
+ }
86
142
  /**
87
143
  * Check if a name starts with any of the approved prefixes, their plural forms,
88
144
  * or if it starts with an underscore (which indicates a private/internal property)
89
145
  */
90
146
  function hasApprovedPrefix(name, options) {
91
147
  const treatLeadingUnderscoreAsApproved = options?.treatLeadingUnderscoreAsApproved ?? true;
92
- const normalizedName = name.startsWith('_') ? name.slice(1) : name;
93
148
  // Skip checking properties that start with an underscore (private/internal properties)
94
149
  if (treatLeadingUnderscoreAsApproved && name.startsWith('_')) {
95
150
  return true;
96
151
  }
97
- return approvedPrefixes.some((prefix) => {
98
- // Check for exact prefix match
99
- if (normalizedName.toLowerCase().startsWith(prefix.toLowerCase())) {
100
- return true;
101
- }
102
- // Check for plural form of the prefix
103
- // Only apply pluralization to certain prefixes that have meaningful plural forms
104
- if (['is', 'has', 'does', 'was', 'had', 'did'].includes(prefix)) {
105
- const pluralPrefix = pluralize_1.default.plural(prefix);
106
- return normalizedName
107
- .toLowerCase()
108
- .startsWith(pluralPrefix.toLowerCase());
109
- }
110
- return false;
111
- });
152
+ return isPrefixedByBooleanKeyword(name, approvedPrefixes);
112
153
  }
113
154
  function nameSuggestsBoolean(name) {
114
155
  const normalizedName = name.startsWith('_') ? name.slice(1) : name;
@@ -125,9 +166,8 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
125
166
  'authenticated',
126
167
  'authorized',
127
168
  ];
128
- return (hasApprovedPrefix(normalizedName, {
129
- treatLeadingUnderscoreAsApproved: false,
130
- }) || suffixKeywords.some((keyword) => lowerName.endsWith(keyword)));
169
+ return (isPrefixedByBooleanKeyword(normalizedName, approvedPrefixes) ||
170
+ suffixKeywords.some((keyword) => lowerName.endsWith(keyword)));
131
171
  }
132
172
  /**
133
173
  * Capitalize the first letter of a name for use in suggested alternatives
@@ -205,94 +245,20 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
205
245
  // Check for logical expressions (&&)
206
246
  if (node.init.type === utils_1.AST_NODE_TYPES.LogicalExpression &&
207
247
  node.init.operator === '&&') {
208
- // Check if the right side is a method call that might return a non-boolean value
209
- const rightSide = node.init.right;
210
- if (rightSide.type === utils_1.AST_NODE_TYPES.CallExpression) {
211
- // If right side is a simple identifier call
212
- if (rightSide.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
213
- const calleeName = rightSide.callee.name;
214
- const lowerCallee = calleeName.toLowerCase();
215
- // For assert*-style utilities, only treat as boolean if we can confirm boolean return type
216
- if (lowerCallee.startsWith('assert')) {
217
- return identifierReturnsBoolean(calleeName);
218
- }
219
- // Otherwise, infer based on naming heuristics
220
- const isBooleanCall = approvedPrefixes.some((prefix) => prefix !== 'asserts' &&
221
- lowerCallee.startsWith(prefix.toLowerCase()));
222
- if (isBooleanCall ||
223
- lowerCallee.includes('boolean') ||
224
- lowerCallee.includes('enabled') ||
225
- lowerCallee.includes('auth') ||
226
- lowerCallee.includes('valid') ||
227
- lowerCallee.includes('check')) {
228
- return true;
229
- }
230
- if (lowerCallee.startsWith('get') ||
231
- lowerCallee.startsWith('fetch') ||
232
- lowerCallee.startsWith('retrieve') ||
233
- lowerCallee.startsWith('load') ||
234
- lowerCallee.startsWith('read')) {
235
- return false;
236
- }
237
- }
238
- // If the method name doesn't suggest it returns a boolean, don't flag it
239
- if (rightSide.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
240
- rightSide.callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
241
- const methodName = rightSide.callee.property.name;
242
- const lowerMethodName = methodName.toLowerCase();
243
- // Ignore assert*-style methods which often return the input value
244
- if (lowerMethodName.startsWith('assert')) {
245
- return false;
246
- }
247
- // Check if the method name suggests it returns a boolean
248
- const isBooleanMethod = approvedPrefixes.some((prefix) => prefix !== 'asserts' &&
249
- lowerMethodName.startsWith(prefix.toLowerCase()));
250
- // If the method name suggests it returns a boolean (starts with a boolean prefix or contains 'boolean' or 'enabled'),
251
- // then the variable should be treated as a boolean
252
- if (isBooleanMethod ||
253
- lowerMethodName.includes('boolean') ||
254
- lowerMethodName.includes('enabled') ||
255
- lowerMethodName.includes('auth') ||
256
- lowerMethodName.includes('valid') ||
257
- lowerMethodName.includes('check')) {
258
- return true;
259
- }
260
- // For methods like getVolume(), getData(), etc., assume they return non-boolean values
261
- if (lowerMethodName.startsWith('get') ||
262
- lowerMethodName.startsWith('fetch') ||
263
- lowerMethodName.startsWith('retrieve') ||
264
- lowerMethodName.startsWith('load') ||
265
- lowerMethodName.startsWith('read')) {
266
- return false;
267
- }
268
- }
248
+ const left = evaluateBooleanishExpression(node.init.left);
249
+ const right = evaluateBooleanishExpression(node.init.right);
250
+ // If both sides are boolean, the result is boolean.
251
+ if (left === 'boolean' && right === 'boolean') {
252
+ return true;
269
253
  }
270
- // Check if the right side is a property access that might return a non-boolean value
271
- if (rightSide.type === utils_1.AST_NODE_TYPES.MemberExpression &&
272
- rightSide.property.type === utils_1.AST_NODE_TYPES.Identifier) {
273
- const propertyName = rightSide.property.name;
274
- const lowerPropertyName = propertyName.toLowerCase();
275
- // If the property name is 'parentElement', 'parentNode', etc., it's likely not a boolean
276
- if (lowerPropertyName.includes('parent') ||
277
- lowerPropertyName.includes('element') ||
278
- lowerPropertyName.includes('node') ||
279
- lowerPropertyName.includes('child') ||
280
- lowerPropertyName.includes('sibling')) {
281
- return false;
282
- }
283
- // Ignore assert*-style properties which often return the input value
284
- if (lowerPropertyName.startsWith('assert')) {
285
- return false;
286
- }
287
- // For property access like user.isAuthenticated, treat as boolean
288
- const isBooleanProperty = approvedPrefixes.some((prefix) => prefix !== 'asserts' &&
289
- lowerPropertyName.startsWith(prefix.toLowerCase()));
290
- if (isBooleanProperty) {
291
- return true;
292
- }
254
+ // If the right side is boolean, and the left side is unknown (but not non-boolean),
255
+ // we treat it as boolean to avoid false negatives for common patterns like `user && user.isActive`.
256
+ // This is a trade-off: it might cause some false positives for non-boolean variables
257
+ // used as guards, but those are less common than the `user && user.isActive` pattern.
258
+ if (right === 'boolean' && left === 'unknown') {
259
+ return true;
293
260
  }
294
- // Default to true for other cases with && to avoid false negatives
295
- return true;
261
+ return false;
296
262
  }
297
263
  // Special case for logical OR (||) - only consider it boolean if:
298
264
  // 1. It's used with boolean literals or
@@ -330,8 +296,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
330
296
  if (lowerCallee.startsWith('assert')) {
331
297
  return identifierReturnsBoolean(calleeName);
332
298
  }
333
- return approvedPrefixes.some((prefix) => prefix !== 'asserts' &&
334
- lowerCallee.startsWith(prefix.toLowerCase()));
299
+ return isPrefixedByBooleanKeyword(calleeName, approvedPrefixesWithoutAsserts);
335
300
  }
336
301
  // Default to false for other cases with || to avoid false positives
337
302
  return false;
@@ -351,8 +316,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
351
316
  return identifierReturnsBoolean(calleeName);
352
317
  }
353
318
  // Check if the function name suggests it returns a boolean
354
- return approvedPrefixes.some((prefix) => prefix !== 'asserts' &&
355
- lowerCallee.startsWith(prefix.toLowerCase()));
319
+ return isPrefixedByBooleanKeyword(calleeName, approvedPrefixes.filter((p) => p !== 'asserts'));
356
320
  }
357
321
  }
358
322
  return false;
@@ -435,8 +399,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
435
399
  if (lowerCallee.startsWith('assert')) {
436
400
  return identifierReturnsBoolean(calleeName) ? 'boolean' : 'unknown';
437
401
  }
438
- const matchesPrefix = approvedPrefixes.some((prefix) => prefix !== 'asserts' &&
439
- lowerCallee.startsWith(prefix.toLowerCase()));
402
+ const matchesPrefix = isPrefixedByBooleanKeyword(calleeName, approvedPrefixesWithoutAsserts);
440
403
  if (matchesPrefix ||
441
404
  lowerCallee.includes('boolean') ||
442
405
  lowerCallee.includes('enabled') ||
@@ -460,8 +423,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
460
423
  if (lowerMethodName.startsWith('assert')) {
461
424
  return 'unknown';
462
425
  }
463
- const matchesPrefix = approvedPrefixes.some((prefix) => prefix !== 'asserts' &&
464
- lowerMethodName.startsWith(prefix.toLowerCase()));
426
+ const matchesPrefix = isPrefixedByBooleanKeyword(methodName, approvedPrefixesWithoutAsserts);
465
427
  if (matchesPrefix ||
466
428
  lowerMethodName.includes('boolean') ||
467
429
  lowerMethodName.includes('enabled') ||
@@ -475,7 +437,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
475
437
  }
476
438
  function evaluateBooleanishExpression(expression) {
477
439
  if (!expression)
478
- return 'nonBoolean';
440
+ return 'unknown';
479
441
  let currentExpression = expression;
480
442
  if (currentExpression.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
481
443
  currentExpression.type === utils_1.AST_NODE_TYPES.TSTypeAssertion ||
@@ -825,10 +787,10 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
825
787
  });
826
788
  }
827
789
  /**
828
- * Check if a variable is used in a while loop condition and is likely a boolean
790
+ * Check if a variable is initialized with a boolean-suggesting member expression
829
791
  * This helps identify variables that should be flagged as needing a boolean prefix
830
792
  */
831
- function isLikelyBooleanInWhileLoop(node) {
793
+ function isLikelyBooleanByMemberExpression(node) {
832
794
  // Check if the variable is initialized with a boolean-related value
833
795
  const variableDeclarator = node.parent;
834
796
  if (variableDeclarator?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
@@ -844,7 +806,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
844
806
  init.property.type === utils_1.AST_NODE_TYPES.Identifier) {
845
807
  const propertyName = init.property.name;
846
808
  // If the property name suggests it's a boolean (starts with a boolean prefix)
847
- const isBooleanProperty = approvedPrefixes.some((prefix) => propertyName.toLowerCase().startsWith(prefix.toLowerCase()));
809
+ const isBooleanProperty = isPrefixedByBooleanKeyword(propertyName, approvedPrefixes);
848
810
  if (isBooleanProperty) {
849
811
  return true;
850
812
  }
@@ -895,8 +857,8 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
895
857
  return;
896
858
  // Check if it's a boolean variable
897
859
  let isBooleanVar = hasBooleanTypeAnnotation(node.id) || hasInitialBooleanValue(node);
898
- // Check if it's a boolean variable used in a while loop
899
- if (!isBooleanVar && isLikelyBooleanInWhileLoop(node.id)) {
860
+ // Check if it's a boolean variable used in a while loop or initialized with a boolean property
861
+ if (!isBooleanVar && isLikelyBooleanByMemberExpression(node.id)) {
900
862
  isBooleanVar = true;
901
863
  }
902
864
  // Check if it's an arrow function with boolean return type
@@ -1074,6 +1036,11 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
1074
1036
  * Check property signatures in interfaces/types for boolean types
1075
1037
  */
1076
1038
  function checkPropertySignature(node) {
1039
+ // Interface/type-alias property names are commonly imposed by contracts the
1040
+ // author cannot rename (external APIs, data-model schemas), so they are only
1041
+ // enforced when explicitly opted in.
1042
+ if (!enforceForPropertySignatures)
1043
+ return;
1077
1044
  if (node.key.type !== utils_1.AST_NODE_TYPES.Identifier)
1078
1045
  return;
1079
1046
  const propertyName = node.key.name;
@@ -1,9 +1,13 @@
1
1
  export declare const RULE_NAME = "enforce-dynamic-imports";
2
2
  type Options = [
3
3
  {
4
- libraries: string[];
4
+ libraries?: string[];
5
+ ignoredLibraries?: string[];
6
+ internalPrefixes?: string[];
5
7
  allowImportType?: boolean;
6
8
  }
7
9
  ];
10
+ export declare const DEFAULT_IGNORED_LIBRARIES: string[];
11
+ export declare const DEFAULT_INTERNAL_PREFIXES: string[];
8
12
  declare const _default: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"dynamicImportRequired", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
9
13
  export default _default;
@@ -1,14 +1,39 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RULE_NAME = void 0;
3
+ exports.DEFAULT_INTERNAL_PREFIXES = exports.DEFAULT_IGNORED_LIBRARIES = exports.RULE_NAME = void 0;
4
4
  const createRule_1 = require("../utils/createRule");
5
+ const minimatch_1 = require("minimatch");
6
+ const module_1 = require("module");
5
7
  exports.RULE_NAME = 'enforce-dynamic-imports';
8
+ exports.DEFAULT_IGNORED_LIBRARIES = [
9
+ 'react',
10
+ 'react/**',
11
+ 'react-dom',
12
+ 'react-dom/**',
13
+ 'next',
14
+ 'next/**',
15
+ '@mui/material',
16
+ '@mui/material/**',
17
+ '@mui/icons-material',
18
+ '@mui/icons-material/**',
19
+ '@emotion/**',
20
+ 'clsx',
21
+ 'tailwind-merge',
22
+ ];
23
+ exports.DEFAULT_INTERNAL_PREFIXES = ['src/', 'functions/'];
24
+ // Pre-built set of Node.js core module names for O(1) lookup.
25
+ const NODE_BUILTINS = new Set(module_1.builtinModules);
26
+ // Returns true for any source that resolves to a Node builtin: bare name
27
+ // ('crypto'), node:-prefixed ('node:fs'), or path form ('fs/promises').
28
+ const isNodeBuiltin = (source) => source.startsWith('node:') ||
29
+ NODE_BUILTINS.has(source) ||
30
+ NODE_BUILTINS.has(source.split('/')[0]);
6
31
  exports.default = (0, createRule_1.createRule)({
7
32
  name: exports.RULE_NAME,
8
33
  meta: {
9
34
  type: 'suggestion',
10
35
  docs: {
11
- description: 'Enforce dynamic imports for specified libraries to optimize bundle size',
36
+ description: 'Enforce dynamic imports for external libraries by default to optimize bundle size, unless explicitly ignored',
12
37
  recommended: 'error',
13
38
  },
14
39
  schema: [
@@ -21,6 +46,18 @@ exports.default = (0, createRule_1.createRule)({
21
46
  type: 'string',
22
47
  },
23
48
  },
49
+ ignoredLibraries: {
50
+ type: 'array',
51
+ items: {
52
+ type: 'string',
53
+ },
54
+ },
55
+ internalPrefixes: {
56
+ type: 'array',
57
+ items: {
58
+ type: 'string',
59
+ },
60
+ },
24
61
  allowImportType: {
25
62
  type: 'boolean',
26
63
  },
@@ -29,46 +66,80 @@ exports.default = (0, createRule_1.createRule)({
29
66
  },
30
67
  ],
31
68
  messages: {
32
- dynamicImportRequired: 'Static import from "{{source}}" eagerly pulls the entire package into the entry bundle, inflating download size and delaying the first render. Load it lazily with a dynamic import (for example, useDynamic(() => import("{{source}}"))) so the code is fetched only when needed; if you only need types, use a type-only import with allowImportType enabled.',
69
+ 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 typeonly import when you only need types.',
33
70
  },
34
71
  },
35
72
  defaultOptions: [
36
73
  {
37
- libraries: ['@stream-io/video-react-sdk'],
74
+ ignoredLibraries: exports.DEFAULT_IGNORED_LIBRARIES,
75
+ internalPrefixes: exports.DEFAULT_INTERNAL_PREFIXES,
38
76
  allowImportType: true,
39
77
  },
40
78
  ],
41
79
  create(context, [options]) {
42
- const { libraries, allowImportType = true } = options;
43
- // Check if the import source matches any of the specified libraries
44
- const isLibraryMatch = (source) => {
45
- return libraries.some((lib) => {
46
- // Simple glob pattern matching
47
- if (lib.includes('*')) {
48
- const pattern = lib.replace(/\*/g, '.*');
49
- const regex = new RegExp(`^${pattern}$`);
50
- return regex.test(source);
80
+ const { libraries, ignoredLibraries = exports.DEFAULT_IGNORED_LIBRARIES, internalPrefixes = exports.DEFAULT_INTERNAL_PREFIXES, allowImportType = true, } = options;
81
+ // When `libraries` is provided, the rule operates in whitelist mode:
82
+ // only the explicitly listed libraries are enforced. This preserves
83
+ // backwards-compatibility with pre-1.16.0 consumer configurations.
84
+ // When `libraries` is absent, enforce-by-default mode applies:
85
+ // all external imports are flagged unless in `ignoredLibraries`.
86
+ const isWhitelistMode = libraries !== undefined;
87
+ // Build an O(1) + glob matcher from a list of library patterns.
88
+ const buildMatcher = (list) => {
89
+ const exactSet = new Set();
90
+ const globs = [];
91
+ for (const lib of list) {
92
+ const mm = new minimatch_1.Minimatch(lib);
93
+ if (mm.hasMagic()) {
94
+ globs.push(mm);
95
+ }
96
+ else {
97
+ exactSet.add(lib);
51
98
  }
52
- return source === lib;
53
- });
99
+ }
100
+ return (source) => exactSet.has(source) || globs.some((mm) => mm.match(source));
101
+ };
102
+ // In whitelist mode, `libraries` is defined (checked above). In
103
+ // enforce-by-default mode, `ignoredLibraries` is used instead.
104
+ const isListedInWhitelist = buildMatcher(libraries ?? []);
105
+ const isIgnoredLibrary = buildMatcher(ignoredLibraries);
106
+ // A source is external only if it looks like an npm package specifier AND
107
+ // is not a known-internal path. Node builtins and configured internal
108
+ // prefixes (e.g. src/, functions/) are excluded to avoid false positives
109
+ // on TypeScript baseUrl imports and Node core modules.
110
+ const isExternal = (source) => {
111
+ return (/^[a-z0-9@]/i.test(source) &&
112
+ !source.startsWith('@/') &&
113
+ !isNodeBuiltin(source) &&
114
+ !internalPrefixes.some((prefix) => source.startsWith(prefix)));
54
115
  };
55
116
  return {
56
117
  ImportDeclaration(node) {
57
118
  const importSource = node.source.value;
58
119
  // Skip type-only imports if allowed
59
120
  if (allowImportType) {
60
- // Check if it's a type-only import declaration
61
121
  if (node.importKind === 'type') {
62
122
  return;
63
123
  }
64
- // Check if all specifiers are type imports
65
124
  if (node.specifiers.length > 0 &&
66
125
  node.specifiers.every((spec) => spec.type === 'ImportSpecifier' && spec.importKind === 'type')) {
67
126
  return;
68
127
  }
69
128
  }
70
- // Check if the import is from a library that should be dynamically imported
71
- if (typeof importSource === 'string' && isLibraryMatch(importSource)) {
129
+ let shouldReport;
130
+ if (isWhitelistMode) {
131
+ // Whitelist mode: report only if the source matches an explicitly
132
+ // listed library. External detection and ignoredLibraries are not
133
+ // consulted, so unlisted npm packages are silently allowed.
134
+ shouldReport = isListedInWhitelist(importSource);
135
+ }
136
+ else {
137
+ // Enforce-by-default mode: report if the source is an external
138
+ // package and is not in the ignored list.
139
+ shouldReport =
140
+ isExternal(importSource) && !isIgnoredLibrary(importSource);
141
+ }
142
+ if (shouldReport) {
72
143
  context.report({
73
144
  node,
74
145
  messageId: 'dynamicImportRequired',
@@ -4,10 +4,7 @@ exports.enforceMemoizeAsync = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
6
  const MEMOIZE_MODULE = '@blumintinc/typescript-memoize';
7
- const ALLOWED_MEMOIZE_MODULES = new Set([
8
- MEMOIZE_MODULE,
9
- 'typescript-memoize',
10
- ]);
7
+ const ALLOWED_MEMOIZE_MODULES = new Set([MEMOIZE_MODULE, 'typescript-memoize']);
11
8
  /**
12
9
  * Matches a memoize decorator in supported syntaxes:
13
10
  * - @Alias()
@@ -3,6 +3,39 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.enforceMuiRoundedIcons = void 0;
4
4
  const createRule_1 = require("../utils/createRule");
5
5
  const utils_1 = require("@typescript-eslint/utils");
6
+ /**
7
+ * Non-Rounded MUI variant suffixes. An icon imported as one of these variants
8
+ * (e.g. `AddReactionOutlined`) maps to the Rounded variant of its BASE name
9
+ * (`AddReactionRounded`), not `AddReactionOutlinedRounded` (which doesn't exist).
10
+ * Matched exactly, so a distinct icon like `MailOutline` (ends in "Outline", not
11
+ * "Outlined") is left intact → `MailOutlineRounded`, which does exist.
12
+ */
13
+ const VARIANT_SUFFIXES = ['Outlined', 'Sharp', 'TwoTone'];
14
+ /**
15
+ * @mui/icons-material brand icons that ship in a single (Filled) variant only —
16
+ * they have no Rounded counterpart, so the rule must not demand one. Computed
17
+ * from @mui/icons-material (the only base icons lacking a `*Rounded` sibling).
18
+ */
19
+ const ICONS_WITHOUT_ROUNDED = new Set([
20
+ 'Apple',
21
+ 'GitHub',
22
+ 'Google',
23
+ 'Instagram',
24
+ 'LinkedIn',
25
+ 'Microsoft',
26
+ 'Pinterest',
27
+ 'Reddit',
28
+ 'Telegram',
29
+ 'Twitter',
30
+ 'WhatsApp',
31
+ 'X',
32
+ 'YouTube',
33
+ ]);
34
+ /** Strips a trailing non-Rounded variant suffix to recover the base icon name. */
35
+ const toBaseIconName = (iconName) => {
36
+ const suffix = VARIANT_SUFFIXES.find((variant) => iconName.endsWith(variant));
37
+ return suffix ? iconName.slice(0, -suffix.length) : iconName;
38
+ };
6
39
  exports.enforceMuiRoundedIcons = (0, createRule_1.createRule)({
7
40
  name: 'enforce-mui-rounded-icons',
8
41
  meta: {
@@ -36,8 +69,16 @@ exports.enforceMuiRoundedIcons = (0, createRule_1.createRule)({
36
69
  if (!iconName) {
37
70
  return;
38
71
  }
72
+ // Map a non-Rounded variant import to its base icon so the Rounded
73
+ // suggestion/fix targets a name that actually exists.
74
+ const baseIconName = toBaseIconName(iconName);
75
+ // Brand icons have no Rounded variant — demanding one is a false
76
+ // positive and the fix would point at a non-existent module.
77
+ if (ICONS_WITHOUT_ROUNDED.has(baseIconName)) {
78
+ return;
79
+ }
39
80
  // Create the rounded variant name
40
- const roundedVariant = `${iconName}Rounded`;
81
+ const roundedVariant = `${baseIconName}Rounded`;
41
82
  context.report({
42
83
  node,
43
84
  messageId: 'enforceRoundedVariant',
@@ -20,6 +20,7 @@ const ALLOWLIST = {
20
20
  domain: new Set([
21
21
  'ai',
22
22
  'blumint',
23
+ 'main',
23
24
  'middleware',
24
25
  'noop',
25
26
  'nth',
@@ -406,6 +407,8 @@ const ALLOWLIST = {
406
407
  'brush',
407
408
  'bubble',
408
409
  'buck',
410
+ 'bucket',
411
+ 'bucketize',
409
412
  'buckle',
410
413
  'bud',
411
414
  'budge',
@@ -158,6 +158,15 @@ exports.default = (0, createRule_1.createRule)({
158
158
  if (target.type === utils_1.AST_NODE_TYPES.Literal && 'regex' in target) {
159
159
  return false;
160
160
  }
161
+ // Skip null and boolean literals. `null as const` is invalid
162
+ // TypeScript (TS1355), so the autofix would produce uncompilable
163
+ // code; `true`/`false` already have literal types, so `as const`
164
+ // is redundant. (`undefined` is an Identifier, not a Literal, so
165
+ // it never reaches the literal branch below.)
166
+ if (target.type === utils_1.AST_NODE_TYPES.Literal &&
167
+ (target.value === null || typeof target.value === 'boolean')) {
168
+ return false;
169
+ }
161
170
  return (target.type === utils_1.AST_NODE_TYPES.Literal ||
162
171
  target.type === utils_1.AST_NODE_TYPES.ArrayExpression ||
163
172
  target.type === utils_1.AST_NODE_TYPES.ObjectExpression);