@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 +1 -1
- package/lib/rules/require-image-optimized.js +87 -29
- package/package.json +1 -1
- package/release-manifest.json +14 -0
package/lib/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
package/release-manifest.json
CHANGED
|
@@ -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",
|