@blumintinc/eslint-plugin-blumint 1.20.81 → 1.20.82

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/lib/index.js CHANGED
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.81',
226
+ version: '1.20.82',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -62,21 +62,75 @@ const isInsideComponentMock = (node, componentModule) => {
62
62
  }
63
63
  return false;
64
64
  };
65
+ /**
66
+ * A type-only specifier binds no value: it renders nothing, so it neither
67
+ * bypasses the optimization pipeline nor can back a fix. The modifier lives
68
+ * either on the specifier (`{ type Image }`) or on the whole declaration
69
+ * (`import type ...`).
70
+ */
71
+ const isTypeOnlySpecifier = (specifier) => {
72
+ if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
73
+ specifier.importKind === 'type') {
74
+ return true;
75
+ }
76
+ return (specifier.parent?.importKind ===
77
+ 'type');
78
+ };
65
79
  /** A type-only binding cannot be rendered, so it is no basis for a fix. */
66
80
  const isTypeOnlyImport = (definition) => {
67
81
  const { node } = definition;
68
- if (node.type === utils_1.AST_NODE_TYPES.ImportSpecifier) {
69
- return (node.importKind === 'type' ||
70
- node.parent?.importKind ===
71
- 'type');
72
- }
73
- if (node.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier ||
82
+ if (node.type === utils_1.AST_NODE_TYPES.ImportSpecifier ||
83
+ node.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier ||
74
84
  node.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
75
- return (node.parent?.importKind ===
76
- 'type');
85
+ return isTypeOnlySpecifier(node);
77
86
  }
78
87
  return false;
79
88
  };
89
+ /**
90
+ * Whether a specifier binds `next/image`'s Image component. The default export
91
+ * *is* that component whatever local name it is bound to, so the binding's
92
+ * identity decides rather than the local name — otherwise `import Img from
93
+ * 'next/image'` becomes a rename-shaped bypass of the rule. `{ default as X }`
94
+ * is the same binding written differently. The named `Image` form is matched
95
+ * too, while every other named export (`getImageProps`, the prop types) is
96
+ * left alone: those are not the optimization bypass.
97
+ */
98
+ const bindsImageComponent = (specifier) => {
99
+ if (isTypeOnlySpecifier(specifier)) {
100
+ return false;
101
+ }
102
+ if (specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier) {
103
+ return true;
104
+ }
105
+ // A namespace binds the module rather than the component, and is consumed
106
+ // through a member expression the fix has no shape for.
107
+ if (specifier.type !== utils_1.AST_NODE_TYPES.ImportSpecifier) {
108
+ return false;
109
+ }
110
+ return (specifier.imported.name === 'default' || specifier.imported.name === 'Image');
111
+ };
112
+ /**
113
+ * The declaration text that keeps the specifiers the fix does not move pointed
114
+ * at their original source. Rewriting the whole declaration would drop them
115
+ * while they are still referenced, and their bindings (`ImageProps`,
116
+ * `getImageProps`) come from `next/image` alone — the wrapper does not
117
+ * re-export them.
118
+ */
119
+ const retainedImportText = (specifiers, sourceCode, source) => {
120
+ const named = specifiers.filter((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier);
121
+ const standalone = specifiers.filter((specifier) => specifier.type !== utils_1.AST_NODE_TYPES.ImportSpecifier);
122
+ const clauses = [
123
+ ...standalone.map((specifier) => sourceCode.getText(specifier)),
124
+ ...(named.length > 0
125
+ ? [
126
+ `{ ${named
127
+ .map((specifier) => sourceCode.getText(specifier))
128
+ .join(', ')} }`,
129
+ ]
130
+ : []),
131
+ ];
132
+ return `import ${clauses.join(', ')} from ${sourceCode.getText(source)};`;
133
+ };
80
134
  const isBoundAsValue = (scope, name) => {
81
135
  const variable = utils_1.ASTUtils.findVariable(scope, name);
82
136
  return (!!variable &&
@@ -213,28 +267,32 @@ module.exports = (0, createRule_1.createRule)({
213
267
  if (isComponentImplementationFile) {
214
268
  return;
215
269
  }
216
- if (node.source.value === 'next/image' && node.specifiers.length > 0) {
217
- const imageSpecifier = node.specifiers.find((spec) => (spec.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier ||
218
- spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier) &&
219
- (spec.local.name === 'Image' ||
220
- (spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
221
- spec.imported.name === 'Image')));
222
- if (imageSpecifier) {
223
- const localName = imageSpecifier.local.name;
224
- // Report the import
225
- context.report({
226
- node,
227
- messageId: 'useImageOptimized',
228
- data: {
229
- componentPath,
230
- component: 'next/image',
231
- },
232
- fix(fixer) {
233
- return fixer.replaceText(node, `import ${localName} from '${componentPath}';`);
234
- },
235
- });
236
- }
270
+ if (node.source.value !== 'next/image') {
271
+ return;
272
+ }
273
+ // A default binding comes first in the specifier list, so this prefers
274
+ // it over a redundant named `Image` alongside it.
275
+ const imageSpecifier = node.specifiers.find(bindsImageComponent);
276
+ if (!imageSpecifier) {
277
+ return;
237
278
  }
279
+ const localName = imageSpecifier.local.name;
280
+ const retained = node.specifiers.filter((specifier) => specifier !== imageSpecifier);
281
+ context.report({
282
+ node,
283
+ messageId: 'useImageOptimized',
284
+ data: {
285
+ componentPath,
286
+ component: 'next/image',
287
+ },
288
+ fix(fixer) {
289
+ const swapped = `import ${localName} from '${componentPath}';`;
290
+ if (retained.length === 0) {
291
+ return fixer.replaceText(node, swapped);
292
+ }
293
+ return fixer.replaceText(node, `${retainedImportText(retained, sourceCode, node.source)}\n${swapped}`);
294
+ },
295
+ });
238
296
  },
239
297
  };
240
298
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.81",
3
+ "version": "1.20.82",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "version": "1.20.82",
4
+ "date": "2026-08-02T21:42:12.697Z",
5
+ "rules": [
6
+ {
7
+ "name": "require-image-optimized",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1623
11
+ ],
12
+ "summary": "key next/image detection on the imported binding (closes #1623)"
13
+ }
14
+ ]
15
+ },
2
16
  {
3
17
  "version": "1.20.81",
4
18
  "date": "2026-08-02T12:22:56.245Z",