qleaner 1.2.0 → 1.3.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 +177 -67
- package/command.js +137 -202
- package/controllers/image.js +146 -508
- package/controllers/initialize.js +4 -2
- package/controllers/list.js +21 -7
- package/controllers/summary.js +176 -143
- package/package.json +2 -4
- package/qleaner.config.json +26 -2
- package/test.js +2 -0
- package/utils/astParser.js +224 -0
- package/utils/cache.js +15 -0
- package/utils/constants.js +29 -3
- package/utils/cssImages.js +130 -51
- package/utils/fileProcessing.js +184 -0
- package/utils/graphComparison.js +144 -0
- package/utils/graphOperations.js +141 -0
- package/utils/graphUtils.js +32 -0
- package/utils/imageAstParser.js +220 -0
- package/utils/imageDisplay.js +113 -0
- package/utils/imageExtractors.js +82 -0
- package/utils/imageGraphUtils.js +96 -0
- package/utils/imagePathBuilder.js +50 -0
- package/utils/pathBuilder.js +33 -0
- package/utils/resolver.js +58 -7
- package/utils/summary.js +32 -7
- package/utils/utils.js +139 -89
package/controllers/image.js
CHANGED
|
@@ -1,389 +1,89 @@
|
|
|
1
1
|
const fg = require("fast-glob");
|
|
2
2
|
const fs = require("fs");
|
|
3
3
|
const path = require("path");
|
|
4
|
-
const
|
|
4
|
+
const { parseCode } = require("../utils/astParser");
|
|
5
5
|
const traverse = require("@babel/traverse").default;
|
|
6
|
-
const Table = require("cli-table3");
|
|
7
6
|
const { getCssImages } = require("../utils/cssImages");
|
|
8
|
-
const {
|
|
7
|
+
const { createStepBar } = require("../utils/utils");
|
|
9
8
|
const { initializeCache } = require("../command");
|
|
10
9
|
const { needsRebuild, getFileHash, saveCache } = require("../utils/cache");
|
|
11
10
|
const { normalize } = require("../utils/resolver");
|
|
12
|
-
const {
|
|
13
|
-
const {
|
|
11
|
+
const { buildImagePaths, buildCodePaths } = require("../utils/imagePathBuilder");
|
|
12
|
+
const { createASTTraverser } = require("../utils/imageAstParser");
|
|
13
|
+
const { addToImageGraph } = require("../utils/imageGraphUtils");
|
|
14
|
+
const { displayUnusedImages, handleImageDeletion } = require("../utils/imageDisplay");
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
|
-
*
|
|
17
|
+
* Creates a file node in the image graph for a code file
|
|
18
|
+
* @param {string} filePath - The file path
|
|
19
|
+
* @param {Map} imageGraph - The image graph Map
|
|
17
20
|
*/
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
21
|
+
function createCodeFileNode(filePath, imageGraph) {
|
|
22
|
+
imageGraph.set(filePath, {
|
|
23
|
+
file: filePath,
|
|
24
|
+
size: fs.statSync(filePath).size,
|
|
25
|
+
hash: getFileHash(fs.readFileSync(filePath, "utf8")),
|
|
26
|
+
imports: new Set(),
|
|
27
|
+
importedBy: new Set(),
|
|
28
|
+
isImage: false,
|
|
29
|
+
lastModified: fs.statSync(filePath).mtime.getTime(),
|
|
28
30
|
});
|
|
29
|
-
return results;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
|
-
*
|
|
34
|
+
* Processes a single import and adds it to the image graph
|
|
35
|
+
* @param {Object} importInfo - Import information object
|
|
36
|
+
* @param {string} imageDirectory - The base image directory
|
|
37
|
+
* @param {Map} imageGraph - The image graph Map
|
|
38
|
+
* @param {Object} options - Options object
|
|
34
39
|
*/
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
expr.properties.forEach((prop) => {
|
|
42
|
-
if (prop.type !== "ObjectProperty" || !prop.key || !prop.value) return;
|
|
43
|
-
|
|
44
|
-
const keyName = prop.key.name || prop.key.value;
|
|
45
|
-
|
|
46
|
-
if (!keyName) return;
|
|
47
|
-
|
|
48
|
-
const isImageField =
|
|
49
|
-
keyName.toLowerCase().includes("background") ||
|
|
50
|
-
keyName.toLowerCase().includes("image") ||
|
|
51
|
-
keyName.toLowerCase().includes("mask");
|
|
52
|
-
|
|
53
|
-
if (!isImageField) return;
|
|
54
|
-
|
|
55
|
-
// String literal
|
|
56
|
-
if (prop.value.type === "StringLiteral") {
|
|
57
|
-
const raw = prop.value.value;
|
|
58
|
-
const matches = [...raw.matchAll(URL_EXTRACT_REGEX)];
|
|
59
|
-
if (matches.length > 0) {
|
|
60
|
-
matches.forEach((m) => {
|
|
61
|
-
imports.push({
|
|
62
|
-
file: file,
|
|
63
|
-
source: m[2],
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
} else if (IMAGE_REGEX.test(raw)) {
|
|
67
|
-
imports.push({
|
|
68
|
-
file: file,
|
|
69
|
-
source: raw,
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
}
|
|
40
|
+
function processImageImport(importInfo, imageDirectory, imageGraph, options) {
|
|
41
|
+
const filePath = path.resolve(importInfo.file);
|
|
42
|
+
let importPath = normalize(importInfo.source, imageDirectory, {
|
|
43
|
+
alias: options.alias ? true : false,
|
|
44
|
+
isRootFolderReferenced: options.isRootFolderReferenced ? true : false,
|
|
45
|
+
});
|
|
73
46
|
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
47
|
+
if (importPath && !fs.existsSync(importPath)) {
|
|
48
|
+
if (options.alias) {
|
|
49
|
+
importPath = normalize(importInfo.source, imageDirectory, {
|
|
50
|
+
alias: false,
|
|
51
|
+
isRootFolderReferenced: true,
|
|
52
|
+
});
|
|
53
|
+
} else {
|
|
54
|
+
importPath = normalize(importInfo.source, imageDirectory, {
|
|
55
|
+
alias: true,
|
|
56
|
+
isRootFolderReferenced: false,
|
|
81
57
|
});
|
|
82
58
|
}
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Build glob patterns for image files with exclusions
|
|
88
|
-
*/
|
|
89
|
-
function buildImagePaths(imageDirectory, options) {
|
|
90
|
-
const imagePaths = [`${imageDirectory}/**/*.{png,jpg,jpeg,svg,gif,webp}`];
|
|
91
|
-
|
|
92
|
-
if (options.excludeDirAssets && options.excludeDirAssets.length > 0) {
|
|
93
|
-
options.excludeDirAssets.forEach((dir) => {
|
|
94
|
-
imagePaths.push(`!${dir}/**`);
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
if (options.excludeFileAssets && options.excludeFileAssets.length > 0) {
|
|
98
|
-
options.excludeFileAssets.forEach((file) => {
|
|
99
|
-
imagePaths.push(`!${imageDirectory}/**/${file}`);
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return imagePaths;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Build glob patterns for code files with exclusions
|
|
108
|
-
*/
|
|
109
|
-
function buildCodePaths(codeDirectory, options) {
|
|
110
|
-
const codePaths = [`${codeDirectory}/**/*.{js,jsx,ts,tsx}`];
|
|
111
|
-
|
|
112
|
-
if (options.excludeDirCode && options.excludeDirCode.length > 0) {
|
|
113
|
-
options.excludeDirCode.forEach((dir) => {
|
|
114
|
-
codePaths.push(`!${dir}/**`);
|
|
115
|
-
});
|
|
59
|
+
addToImageGraph(importPath, imageGraph);
|
|
60
|
+
} else {
|
|
61
|
+
addToImageGraph(importPath, imageGraph);
|
|
116
62
|
}
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
});
|
|
63
|
+
if (importPath) {
|
|
64
|
+
imageGraph.get(importPath).importedBy.add(filePath);
|
|
65
|
+
imageGraph.get(filePath).imports.add(importPath);
|
|
121
66
|
}
|
|
122
|
-
|
|
123
|
-
return codePaths;
|
|
124
67
|
}
|
|
125
68
|
|
|
126
69
|
/**
|
|
127
|
-
*
|
|
70
|
+
* Processes all collected image imports
|
|
71
|
+
* @param {Array} imports - Array of import information objects
|
|
72
|
+
* @param {Function} createStepBar - Function to create progress bar
|
|
73
|
+
* @param {string} imageDirectory - The base image directory
|
|
74
|
+
* @param {Map} imageGraph - The image graph Map
|
|
75
|
+
* @param {Object} options - Options object
|
|
76
|
+
* @param {Object} chalk - Chalk instance for colored output
|
|
128
77
|
*/
|
|
129
|
-
function
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
* import logo from './img/a.png'
|
|
133
|
-
*/
|
|
134
|
-
ImportDeclaration(pathNode) {
|
|
135
|
-
const val = pathNode.node.source.value;
|
|
136
|
-
if (IMAGE_REGEX.test(val)) {
|
|
137
|
-
imports.push({
|
|
138
|
-
file: file,
|
|
139
|
-
source: val,
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* require('./img/a.png')
|
|
146
|
-
*/
|
|
147
|
-
CallExpression(pathNode) {
|
|
148
|
-
const callee = pathNode.node.callee;
|
|
149
|
-
const args = pathNode.node.arguments;
|
|
150
|
-
|
|
151
|
-
if (
|
|
152
|
-
callee.type === "Identifier" &&
|
|
153
|
-
callee.name === "require" &&
|
|
154
|
-
args.length &&
|
|
155
|
-
args[0].type === "StringLiteral"
|
|
156
|
-
) {
|
|
157
|
-
const val = args[0].value;
|
|
158
|
-
if (IMAGE_REGEX.test(val)) {
|
|
159
|
-
imports.push({
|
|
160
|
-
file: file,
|
|
161
|
-
source: val,
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// dynamic import("./a.png")
|
|
167
|
-
if (
|
|
168
|
-
callee.type === "Import" &&
|
|
169
|
-
args.length &&
|
|
170
|
-
args[0].type === "StringLiteral"
|
|
171
|
-
) {
|
|
172
|
-
const val = args[0].value;
|
|
173
|
-
if (IMAGE_REGEX.test(val)) {
|
|
174
|
-
imports.push({
|
|
175
|
-
file: file,
|
|
176
|
-
source: val,
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
},
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* <img src="...">
|
|
184
|
-
*/
|
|
185
|
-
JSXAttribute(attr) {
|
|
186
|
-
if (attr.node.name.name === "src") {
|
|
187
|
-
const v = attr.node.value;
|
|
188
|
-
if (!v) return;
|
|
189
|
-
|
|
190
|
-
if (v.type === "StringLiteral") {
|
|
191
|
-
if (IMAGE_REGEX.test(v.value)) {
|
|
192
|
-
imports.push({
|
|
193
|
-
file: file,
|
|
194
|
-
source: v.value,
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (v.type === "JSXExpressionContainer") {
|
|
200
|
-
const expr = v.expression;
|
|
201
|
-
|
|
202
|
-
if (expr.type === "StringLiteral") {
|
|
203
|
-
if (IMAGE_REGEX.test(expr.value)) {
|
|
204
|
-
imports.push({
|
|
205
|
-
file: file,
|
|
206
|
-
source: expr.value,
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (expr.type === "TemplateLiteral") {
|
|
212
|
-
extractFromTemplateLiteral(expr.quasis).forEach((v) => {
|
|
213
|
-
imports.push({
|
|
214
|
-
file: file,
|
|
215
|
-
source: v,
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Handle style={{ backgroundImage: "url(...)" }}
|
|
223
|
-
if (attr.node.name.name === "style") {
|
|
224
|
-
extractFromJSXStyle(attr.node.value, file, imports);
|
|
225
|
-
}
|
|
226
|
-
},
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Array expressions: const images = ['/img/a.png', '/img/b.png']
|
|
230
|
-
* This explicitly handles arrays of image paths, including spread elements
|
|
231
|
-
*/
|
|
232
|
-
ArrayExpression(p) {
|
|
233
|
-
p.node.elements.forEach((element) => {
|
|
234
|
-
if (!element) return; // Skip null/undefined elements (e.g., [1, , 3])
|
|
235
|
-
|
|
236
|
-
// Handle string literals in arrays
|
|
237
|
-
if (element.type === "StringLiteral") {
|
|
238
|
-
const val = element.value;
|
|
239
|
-
if (IMAGE_REGEX.test(val)) {
|
|
240
|
-
imports.push({
|
|
241
|
-
file: file,
|
|
242
|
-
source: val,
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
// detect url("...") inside strings
|
|
246
|
-
const matches = [...val.matchAll(URL_EXTRACT_REGEX)];
|
|
247
|
-
matches.forEach((m) =>
|
|
248
|
-
imports.push({
|
|
249
|
-
file: file,
|
|
250
|
-
source: m[2],
|
|
251
|
-
})
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Handle template literals in arrays
|
|
256
|
-
if (element.type === "TemplateLiteral") {
|
|
257
|
-
extractFromTemplateLiteral(element.quasis).forEach((v) => {
|
|
258
|
-
imports.push({
|
|
259
|
-
file: file,
|
|
260
|
-
source: v,
|
|
261
|
-
});
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Handle spread elements: [...otherArray, '/img/a.png']
|
|
266
|
-
if (element.type === "SpreadElement" && element.argument) {
|
|
267
|
-
// The spread element's argument will be visited by other handlers
|
|
268
|
-
// but we can explicitly check if it's an array with string literals
|
|
269
|
-
if (element.argument.type === "ArrayExpression") {
|
|
270
|
-
element.argument.elements.forEach((spreadEl) => {
|
|
271
|
-
if (spreadEl && spreadEl.type === "StringLiteral") {
|
|
272
|
-
const val = spreadEl.value;
|
|
273
|
-
if (IMAGE_REGEX.test(val)) {
|
|
274
|
-
imports.push({
|
|
275
|
-
file: file,
|
|
276
|
-
source: val,
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
});
|
|
284
|
-
},
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* String Literals anywhere
|
|
288
|
-
*/
|
|
289
|
-
StringLiteral(p) {
|
|
290
|
-
const val = p.node.value;
|
|
291
|
-
|
|
292
|
-
if (IMAGE_REGEX.test(val)) {
|
|
293
|
-
imports.push({
|
|
294
|
-
file: file,
|
|
295
|
-
source: val,
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// detect url("...") inside strings (e.g., Tailwind)
|
|
300
|
-
const matches = [...val.matchAll(URL_EXTRACT_REGEX)];
|
|
301
|
-
matches.forEach((m) =>
|
|
302
|
-
imports.push({
|
|
303
|
-
file: file,
|
|
304
|
-
source: m[2],
|
|
305
|
-
})
|
|
306
|
-
);
|
|
307
|
-
},
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Template Literals anywhere
|
|
311
|
-
*/
|
|
312
|
-
TemplateLiteral(p) {
|
|
313
|
-
extractFromTemplateLiteral(p.node.quasis).forEach((v) => {
|
|
314
|
-
imports.push({
|
|
315
|
-
file: file,
|
|
316
|
-
source: v,
|
|
317
|
-
});
|
|
318
|
-
});
|
|
319
|
-
},
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* styled-components & css`` blocks
|
|
323
|
-
*/
|
|
324
|
-
TaggedTemplateExpression(p) {
|
|
325
|
-
const quasi = p.node.quasi;
|
|
326
|
-
const extracted = extractFromTemplateLiteral(quasi.quasis);
|
|
327
|
-
extracted.forEach((v) =>
|
|
328
|
-
imports.push({
|
|
329
|
-
file: file,
|
|
330
|
-
source: v,
|
|
331
|
-
})
|
|
332
|
-
);
|
|
333
|
-
},
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Scan code files and extract all image references
|
|
339
|
-
*/
|
|
340
|
-
async function scanCodeFilesForImages(
|
|
341
|
-
chalk,
|
|
342
|
-
codeFiles,
|
|
343
|
-
codeDirectory,
|
|
78
|
+
async function processImageImports(
|
|
79
|
+
imports,
|
|
80
|
+
createStepBar,
|
|
344
81
|
imageDirectory,
|
|
345
82
|
imageGraph,
|
|
346
|
-
options
|
|
83
|
+
options,
|
|
84
|
+
chalk
|
|
347
85
|
) {
|
|
348
|
-
const imports = [];
|
|
349
|
-
const oldPaths = new Map();
|
|
350
|
-
const scanBar = createStepBar(
|
|
351
|
-
`1/${codeFiles.length}`,
|
|
352
|
-
codeFiles.length,
|
|
353
|
-
"Scanning code files",
|
|
354
|
-
chalk
|
|
355
|
-
);
|
|
356
|
-
for (const file of codeFiles) {
|
|
357
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
358
|
-
scanBar.increment();
|
|
359
|
-
const filePath = path.resolve(file);
|
|
360
|
-
const code = fs.readFileSync(filePath, "utf8");
|
|
361
|
-
if (needsRebuild(filePath, code, imageGraph)) {
|
|
362
|
-
const ast = parser.parse(code, {
|
|
363
|
-
sourceType: "module",
|
|
364
|
-
plugins: ["jsx", "typescript", "decorators-legacy"],
|
|
365
|
-
});
|
|
366
|
-
if (imageGraph.has(filePath)) {
|
|
367
|
-
const oldFiles = new Set(imageGraph.get(filePath).imports);
|
|
368
|
-
// Create a copy of the Set to avoid it being cleared/modified when imageGraph is updated
|
|
369
|
-
oldPaths.set(filePath, new Set(oldFiles));
|
|
370
|
-
}
|
|
371
|
-
imageGraph.set(filePath, {
|
|
372
|
-
file: filePath,
|
|
373
|
-
size: fs.statSync(filePath).size,
|
|
374
|
-
hash: getFileHash(fs.readFileSync(filePath, "utf8")),
|
|
375
|
-
imports: new Set(),
|
|
376
|
-
importedBy: new Set(),
|
|
377
|
-
isImage: false,
|
|
378
|
-
lastModified: fs.statSync(filePath).mtime.getTime(),
|
|
379
|
-
});
|
|
380
|
-
const traverser = createASTTraverser(filePath, imports);
|
|
381
|
-
traverse(ast, traverser);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
scanBar.stop();
|
|
385
86
|
let packingBar = null;
|
|
386
|
-
// check if imports is empty
|
|
387
87
|
if (imports.length > 0) {
|
|
388
88
|
packingBar = createStepBar(
|
|
389
89
|
`1/${imports.length}`,
|
|
@@ -392,42 +92,32 @@ async function scanCodeFilesForImages(
|
|
|
392
92
|
chalk
|
|
393
93
|
);
|
|
394
94
|
}
|
|
395
|
-
|
|
95
|
+
|
|
396
96
|
for (const importInfo of imports) {
|
|
397
97
|
if (packingBar) {
|
|
398
98
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
399
99
|
packingBar.increment();
|
|
400
100
|
}
|
|
401
|
-
|
|
402
|
-
let importPath = normalize(importInfo.source, imageDirectory, {
|
|
403
|
-
alias: options.alias ? true : false,
|
|
404
|
-
isRootFolderReferenced: options.isRootFolderReferenced ? true : false,
|
|
405
|
-
});
|
|
406
|
-
if (importPath && !fs.existsSync(importPath)) {
|
|
407
|
-
if(options.alias) {
|
|
408
|
-
importPath = normalize(importInfo.source, imageDirectory, {
|
|
409
|
-
alias: false,
|
|
410
|
-
isRootFolderReferenced: true,
|
|
411
|
-
});
|
|
412
|
-
}else {
|
|
413
|
-
importPath = normalize(importInfo.source, imageDirectory, {
|
|
414
|
-
alias: true,
|
|
415
|
-
isRootFolderReferenced: false,
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
addToImageGraph(importPath, imageGraph);
|
|
419
|
-
} else {
|
|
420
|
-
addToImageGraph(importPath, imageGraph);
|
|
421
|
-
}
|
|
422
|
-
importPath && imageGraph.get(importPath).importedBy.add(filePath);
|
|
423
|
-
importPath && imageGraph.get(filePath).imports.add(importPath);
|
|
101
|
+
processImageImport(importInfo, imageDirectory, imageGraph, options);
|
|
424
102
|
}
|
|
425
103
|
|
|
426
104
|
if (packingBar) {
|
|
427
105
|
packingBar.stop();
|
|
428
106
|
}
|
|
107
|
+
}
|
|
429
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Compares old import paths with new paths and updates the graph
|
|
111
|
+
* @param {Map} oldPaths - Map of file paths to their old import sets
|
|
112
|
+
* @param {Function} createStepBar - Function to create progress bar
|
|
113
|
+
* @param {Map} imageGraph - The image graph Map
|
|
114
|
+
* @param {Object} chalk - Chalk instance for colored output
|
|
115
|
+
* @returns {number} Number of files with removed imports
|
|
116
|
+
*/
|
|
117
|
+
async function compareCodePaths(oldPaths, createStepBar, imageGraph, chalk) {
|
|
118
|
+
let removedFilesCount = 0;
|
|
430
119
|
let compareBar = null;
|
|
120
|
+
|
|
431
121
|
if (oldPaths.size > 0) {
|
|
432
122
|
compareBar = createStepBar(
|
|
433
123
|
`1/${oldPaths.size}`,
|
|
@@ -437,9 +127,6 @@ async function scanCodeFilesForImages(
|
|
|
437
127
|
);
|
|
438
128
|
}
|
|
439
129
|
|
|
440
|
-
// count the number of removed files
|
|
441
|
-
let removedFilesCount = 0;
|
|
442
|
-
// compare old paths with new paths
|
|
443
130
|
for (const [filePath, oldFiles] of oldPaths) {
|
|
444
131
|
if (compareBar) {
|
|
445
132
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
@@ -459,9 +146,78 @@ async function scanCodeFilesForImages(
|
|
|
459
146
|
removedFilesCount++;
|
|
460
147
|
}
|
|
461
148
|
}
|
|
149
|
+
|
|
462
150
|
if (compareBar) {
|
|
463
151
|
compareBar.stop();
|
|
464
152
|
}
|
|
153
|
+
|
|
154
|
+
return removedFilesCount;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Scan code files and extract all image references
|
|
159
|
+
* @param {Object} chalk - Chalk instance for colored output
|
|
160
|
+
* @param {Array<string>} codeFiles - Array of code file paths
|
|
161
|
+
* @param {string} codeDirectory - The base code directory
|
|
162
|
+
* @param {string} imageDirectory - The base image directory
|
|
163
|
+
* @param {Map} imageGraph - The image graph Map
|
|
164
|
+
* @param {Object} options - Options object
|
|
165
|
+
* @returns {Promise<number>} Number indicating if any changes were made
|
|
166
|
+
*/
|
|
167
|
+
async function scanCodeFilesForImages(
|
|
168
|
+
chalk,
|
|
169
|
+
codeFiles,
|
|
170
|
+
codeDirectory,
|
|
171
|
+
imageDirectory,
|
|
172
|
+
imageGraph,
|
|
173
|
+
options
|
|
174
|
+
) {
|
|
175
|
+
const imports = [];
|
|
176
|
+
const oldPaths = new Map();
|
|
177
|
+
const scanBar = createStepBar(
|
|
178
|
+
`1/${codeFiles.length}`,
|
|
179
|
+
codeFiles.length,
|
|
180
|
+
"Scanning code files",
|
|
181
|
+
chalk
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
for (const file of codeFiles) {
|
|
185
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
186
|
+
scanBar.increment();
|
|
187
|
+
const filePath = path.resolve(file);
|
|
188
|
+
const code = fs.readFileSync(filePath, "utf8");
|
|
189
|
+
|
|
190
|
+
if (needsRebuild(filePath, code, imageGraph)) {
|
|
191
|
+
const ast = parseCode(code);
|
|
192
|
+
if (imageGraph.has(filePath)) {
|
|
193
|
+
const oldFiles = new Set(imageGraph.get(filePath).imports);
|
|
194
|
+
// Create a copy of the Set to avoid it being cleared/modified when imageGraph is updated
|
|
195
|
+
oldPaths.set(filePath, new Set(oldFiles));
|
|
196
|
+
}
|
|
197
|
+
createCodeFileNode(filePath, imageGraph);
|
|
198
|
+
const traverser = createASTTraverser(filePath, imports);
|
|
199
|
+
traverse(ast, traverser);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
scanBar.stop();
|
|
204
|
+
|
|
205
|
+
await processImageImports(
|
|
206
|
+
imports,
|
|
207
|
+
createStepBar,
|
|
208
|
+
imageDirectory,
|
|
209
|
+
imageGraph,
|
|
210
|
+
options,
|
|
211
|
+
chalk
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const removedFilesCount = await compareCodePaths(
|
|
215
|
+
oldPaths,
|
|
216
|
+
createStepBar,
|
|
217
|
+
imageGraph,
|
|
218
|
+
chalk
|
|
219
|
+
);
|
|
220
|
+
|
|
465
221
|
// Add CSS images
|
|
466
222
|
const numberOfCssFiles = await getCssImages(
|
|
467
223
|
codeDirectory,
|
|
@@ -471,11 +227,16 @@ async function scanCodeFilesForImages(
|
|
|
471
227
|
options,
|
|
472
228
|
chalk
|
|
473
229
|
);
|
|
230
|
+
|
|
474
231
|
return imports.length || removedFilesCount || numberOfCssFiles;
|
|
475
232
|
}
|
|
476
233
|
|
|
477
234
|
/**
|
|
478
235
|
* Find unused images by comparing image files with normalized used images
|
|
236
|
+
* @param {Object} imageParentGraph - Object containing imageGraph and unusedImages
|
|
237
|
+
* @param {Array<string>} imageFiles - Array of image file paths
|
|
238
|
+
* @param {Object} options - Options object
|
|
239
|
+
* @returns {Set} Set of unused images
|
|
479
240
|
*/
|
|
480
241
|
function findUnusedImages(imageParentGraph, imageFiles, options) {
|
|
481
242
|
for (const imageFile of imageFiles) {
|
|
@@ -504,7 +265,7 @@ function findUnusedImages(imageParentGraph, imageFiles, options) {
|
|
|
504
265
|
return imageParentGraph.unusedImages;
|
|
505
266
|
}
|
|
506
267
|
// find unused images in imageGraph
|
|
507
|
-
for (const [
|
|
268
|
+
for (const [, image] of imageParentGraph.imageGraph) {
|
|
508
269
|
if ((image.importedBy.size === 0 || image.size === 0) && image.isImage) {
|
|
509
270
|
imageParentGraph.unusedImages.add({
|
|
510
271
|
...image,
|
|
@@ -517,104 +278,6 @@ function findUnusedImages(imageParentGraph, imageFiles, options) {
|
|
|
517
278
|
return imageParentGraph.unusedImages;
|
|
518
279
|
}
|
|
519
280
|
|
|
520
|
-
/**
|
|
521
|
-
* Display unused images in table or list format
|
|
522
|
-
*/
|
|
523
|
-
function displayUnusedImages(unusedImages, options, chalk) {
|
|
524
|
-
let totalSize = 0;
|
|
525
|
-
|
|
526
|
-
if (unusedImages.length === 0) {
|
|
527
|
-
console.log(chalk.green.bold("\n✓ No unused images found!"));
|
|
528
|
-
console.log(chalk.green("════════════════════════════════════════════════\n"));
|
|
529
|
-
return;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
if (options.dryRun) {
|
|
533
|
-
console.log(chalk.cyan.bold('\n[DRY RUN MODE] No images will be deleted\n'));
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
if (options.table) {
|
|
537
|
-
const table = new Table({
|
|
538
|
-
head: options.dryRun
|
|
539
|
-
? [
|
|
540
|
-
chalk.cyan("Unused Images (Would Delete)"),
|
|
541
|
-
chalk.cyan("In Code"),
|
|
542
|
-
chalk.cyan("Exists"),
|
|
543
|
-
chalk.cyan("Size"),
|
|
544
|
-
]
|
|
545
|
-
: [
|
|
546
|
-
chalk.cyan("Unused Images"),
|
|
547
|
-
chalk.cyan("In Code"),
|
|
548
|
-
chalk.cyan("Exists"),
|
|
549
|
-
chalk.cyan("Size"),
|
|
550
|
-
],
|
|
551
|
-
colWidths: [75, 10, 10, 15],
|
|
552
|
-
style: { head: [], border: [] }
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
unusedImages.forEach((img) => {
|
|
556
|
-
const inCode = img.hash === null;
|
|
557
|
-
const exists = img.hash !== null;
|
|
558
|
-
const size = img.size > 0 ? (img.size / (1024 * 1024)).toFixed(2) + " MB" : "N/A";
|
|
559
|
-
|
|
560
|
-
table.push([
|
|
561
|
-
chalk.white(formatFilePath(img.file, 70)),
|
|
562
|
-
inCode ? chalk.red("Yes") : chalk.green("No"),
|
|
563
|
-
exists ? chalk.green("Yes") : chalk.red("No"),
|
|
564
|
-
chalk.yellow(size),
|
|
565
|
-
]);
|
|
566
|
-
totalSize += img.size;
|
|
567
|
-
});
|
|
568
|
-
console.log(table.toString());
|
|
569
|
-
} else {
|
|
570
|
-
unusedImages.forEach((img) => {
|
|
571
|
-
const inCode = img.hash === null;
|
|
572
|
-
const exists = img.hash !== null;
|
|
573
|
-
const size = img.size > 0 ? (img.size / (1024 * 1024)).toFixed(2) + " MB" : "N/A";
|
|
574
|
-
|
|
575
|
-
console.log(
|
|
576
|
-
chalk.white("🖼️ ") +
|
|
577
|
-
chalk.blue(formatFilePath(img.file, 85)) +
|
|
578
|
-
" - " +
|
|
579
|
-
chalk.cyan("in code: ") +
|
|
580
|
-
(inCode ? chalk.red("Yes") : chalk.green("No")) +
|
|
581
|
-
" - " +
|
|
582
|
-
chalk.cyan("exists: ") +
|
|
583
|
-
(exists ? chalk.green("Yes") : chalk.red("No")) +
|
|
584
|
-
" - " +
|
|
585
|
-
chalk.cyan("size: ") +
|
|
586
|
-
chalk.yellow(size)
|
|
587
|
-
);
|
|
588
|
-
totalSize += img.size;
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// Summary section
|
|
593
|
-
console.log(chalk.green("\n════════════════════════════════════════════════"));
|
|
594
|
-
console.log(
|
|
595
|
-
chalk.yellow.bold("Total Size: ") +
|
|
596
|
-
chalk.yellow((totalSize / (1024 * 1024)).toFixed(2) + " MB")
|
|
597
|
-
);
|
|
598
|
-
console.log(
|
|
599
|
-
chalk.magenta.bold("Total Images: ") +
|
|
600
|
-
chalk.magenta(unusedImages.size)
|
|
601
|
-
);
|
|
602
|
-
console.log(chalk.green("════════════════════════════════════════════════\n"));
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
/**
|
|
606
|
-
* Handle deletion logic for unused images
|
|
607
|
-
*/
|
|
608
|
-
function handleImageDeletion(unusedImages, options, chalk) {
|
|
609
|
-
if (options.dryRun) {
|
|
610
|
-
console.log(
|
|
611
|
-
chalk.cyan(`\n[DRY RUN] Would delete ${unusedImages.size} file(s)`)
|
|
612
|
-
);
|
|
613
|
-
} else if (unusedImages.size > 0) {
|
|
614
|
-
askDeleteFiles(unusedImages, false);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
281
|
/**
|
|
619
282
|
* MAIN FUNCTION
|
|
620
283
|
*/
|
|
@@ -712,31 +375,6 @@ async function getUnusedImages(
|
|
|
712
375
|
spinner.succeed(`Completed Qleaner scan`);
|
|
713
376
|
}
|
|
714
377
|
|
|
715
|
-
function addToImageGraph(importPath, imageGraph) {
|
|
716
|
-
if (importPath && !imageGraph.has(importPath)) {
|
|
717
|
-
let size = 0;
|
|
718
|
-
let hash = null;
|
|
719
|
-
let lastModified = null;
|
|
720
|
-
try {
|
|
721
|
-
size = fs.statSync(importPath).size;
|
|
722
|
-
hash = getFileHash(fs.readFileSync(importPath, "utf8"));
|
|
723
|
-
lastModified = fs.statSync(importPath).mtime.getTime();
|
|
724
|
-
} catch (error) {
|
|
725
|
-
size = 0;
|
|
726
|
-
hash = null;
|
|
727
|
-
lastModified = null;
|
|
728
|
-
}
|
|
729
|
-
imageGraph.set(importPath, {
|
|
730
|
-
file: importPath,
|
|
731
|
-
size: size,
|
|
732
|
-
hash: hash,
|
|
733
|
-
imports: new Set(),
|
|
734
|
-
importedBy: new Set(),
|
|
735
|
-
lastModified: lastModified,
|
|
736
|
-
isImage: true,
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
378
|
|
|
741
379
|
|
|
742
380
|
module.exports = {
|