qleaner 1.0.16 → 1.0.18
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/bin/cli.js +16 -6
- package/command.js +1 -7
- package/controllers/image.js +191 -0
- package/controllers/list.js +0 -9
- package/package.json +2 -1
- package/{cssImages.js → utils/cssImages.js} +15 -10
- package/utils/resolver.js +1 -1
- package/utils/utils.js +13 -1
package/bin/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
const { Command } = require("commander");
|
|
3
3
|
const { list, scan } = require("../controllers/list");
|
|
4
4
|
const { clearCache } = require("../utils/cache");
|
|
5
|
+
const { getUnusedImages } = require("../controllers/image");
|
|
5
6
|
|
|
6
7
|
async function loadChalk() {
|
|
7
8
|
return (await import("chalk")).default;
|
|
@@ -58,16 +59,25 @@ async function loadChalk() {
|
|
|
58
59
|
program
|
|
59
60
|
.command('qlean-image')
|
|
60
61
|
.description("Scan the project for unused images")
|
|
61
|
-
.argument("<
|
|
62
|
-
.
|
|
63
|
-
.option("-
|
|
62
|
+
.argument("<directory>", "The path to the directory to scan for unused images")
|
|
63
|
+
.argument("<rootPath>", "The root path to the project code utilizing the images")
|
|
64
|
+
.option("-e, --exclude-dir-assets <dir...>", "Exclude directories from the scan")
|
|
65
|
+
.option("-f, --exclude-file-asset <file...>", "Exclude files from the scan")
|
|
64
66
|
.option(
|
|
65
|
-
"-F, --exclude-file-print <files...>",
|
|
67
|
+
"-F, --exclude-file-print-asset <files...>",
|
|
68
|
+
"Scan but don't print the excluded files"
|
|
69
|
+
)
|
|
70
|
+
.option("-E, --exclude-dir-code <dir...>", "Exclude directories from the scan")
|
|
71
|
+
.option("-S, --exclude-file-code <file...>", "Exclude files from the scan")
|
|
72
|
+
.option(
|
|
73
|
+
"-P, --exclude-file-print-code <files...>",
|
|
66
74
|
"Scan but don't print the excluded files"
|
|
67
75
|
)
|
|
68
76
|
.option("-t, --table", "Print the results in a table")
|
|
69
|
-
.
|
|
70
|
-
|
|
77
|
+
.option("-d, --dry-run", "Show what would be deleted without actually deleting (skips prompt)")
|
|
78
|
+
.option("-C, --clear-cache", "Clear the cache")
|
|
79
|
+
.action(async (directory, rootPath, options) => {
|
|
80
|
+
await getUnusedImages(chalk, directory, rootPath, options);
|
|
71
81
|
});
|
|
72
82
|
program.parse(process.argv);
|
|
73
83
|
})();
|
package/command.js
CHANGED
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
needsRebuild,
|
|
12
12
|
saveCache,
|
|
13
13
|
} = require("./utils/cache");
|
|
14
|
+
const { isExcludedFile, compareFiles } = require("./utils/utils");
|
|
14
15
|
|
|
15
16
|
async function getFiles(directory = "src", options, chalk) {
|
|
16
17
|
const contentPaths = [`${directory}/**/*.{tsx,ts,js,jsx}`];
|
|
@@ -176,7 +177,6 @@ async function unUsedFiles(chalk, directory = "src", options) {
|
|
|
176
177
|
let isFound = false;
|
|
177
178
|
while (!isFound && i < imports.length) {
|
|
178
179
|
const importFilePath = await resolver(
|
|
179
|
-
chalk,
|
|
180
180
|
imports[i].file,
|
|
181
181
|
imports[i].from
|
|
182
182
|
);
|
|
@@ -208,13 +208,7 @@ async function unUsedFiles(chalk, directory = "src", options) {
|
|
|
208
208
|
return unusedFiles;
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
function isExcludedFile(file, excludeFiles) {
|
|
212
|
-
return excludeFiles.some((exclude) => file.includes(exclude));
|
|
213
|
-
}
|
|
214
211
|
|
|
215
|
-
function compareFiles(filePath, importPath) {
|
|
216
|
-
return filePath === importPath;
|
|
217
|
-
}
|
|
218
212
|
|
|
219
213
|
module.exports = {
|
|
220
214
|
getFiles,
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
const fg = require("fast-glob");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const parser = require("@babel/parser");
|
|
4
|
+
const traverse = require("@babel/traverse").default;
|
|
5
|
+
const { getCssImages } = require("../utils/cssImages");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const { createResolver } = require("../utils/resolver");
|
|
8
|
+
const { compareFiles, isExcludedFile, askDeleteFiles } = require("../utils/utils");
|
|
9
|
+
const Table = require('cli-table3');
|
|
10
|
+
|
|
11
|
+
async function getUnusedImages(chalk, imageDirectory, codeDirectory, options) {
|
|
12
|
+
const usedImages = [];
|
|
13
|
+
const unusedImages = [];
|
|
14
|
+
const imageRegex = /\.(png|jpg|jpeg|svg|gif|webp)$/i;
|
|
15
|
+
const contentPaths = [`${imageDirectory}/**/*.{png,jpg,jpeg,svg,gif,webp}`];
|
|
16
|
+
if (options.excludeDirAssets && options.excludeDirAssets.length > 0) {
|
|
17
|
+
options.excludeDirAssets.forEach((dir) => {
|
|
18
|
+
contentPaths.push(`!${dir}/**`);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
if (options.excludeFileAsset && options.excludeFileAsset.length > 0) {
|
|
22
|
+
options.excludeFileAsset.forEach((file) => {
|
|
23
|
+
contentPaths.push(`!${imageDirectory}/**/${file}`);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const rootPaths = [`${codeDirectory}/**/*.{js,jsx,ts,tsx}`];
|
|
28
|
+
if (options.excludeDirCode && options.excludeDirCode.length > 0) {
|
|
29
|
+
options.excludeDirCode.forEach((dir) => {
|
|
30
|
+
rootPaths.push(`!${dir}/**`);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (options.excludeFileCode && options.excludeFileCode.length > 0) {
|
|
34
|
+
options.excludeFileCode.forEach((file) => {
|
|
35
|
+
rootPaths.push(`!${codeDirectory}/**/${file}`);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const files = await fg(contentPaths);
|
|
40
|
+
const codeFiles = await fg(rootPaths);
|
|
41
|
+
|
|
42
|
+
for (const file of codeFiles) {
|
|
43
|
+
|
|
44
|
+
const code = fs.readFileSync(file, "utf8");
|
|
45
|
+
const ast = parser.parse(code, {
|
|
46
|
+
sourceType: "module",
|
|
47
|
+
plugins: ["jsx", "typescript"],
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
traverse(ast, {
|
|
51
|
+
// <img src="...">
|
|
52
|
+
JSXAttribute(path) {
|
|
53
|
+
if (path.node.name.name !== "src") return;
|
|
54
|
+
|
|
55
|
+
const value = path.node.value;
|
|
56
|
+
|
|
57
|
+
// String literals: `img/a.png`
|
|
58
|
+
if (value.type === "StringLiteral") {
|
|
59
|
+
if (imageRegex.test(value.value)) {
|
|
60
|
+
usedImages.push({
|
|
61
|
+
value: value.value,
|
|
62
|
+
file,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (value.type === "JSXExpressionContainer") {
|
|
68
|
+
const expr = value.expression;
|
|
69
|
+
// String literals: `img/a.png`
|
|
70
|
+
if (expr.type === "StringLiteral" && imageRegex.test(expr.value)) {
|
|
71
|
+
usedImages.push({
|
|
72
|
+
value: expr.value,
|
|
73
|
+
file,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Template literals: `/images/${name}.png`
|
|
78
|
+
if (expr.type === "TemplateLiteral") {
|
|
79
|
+
expr.quasis.forEach((q) => {
|
|
80
|
+
if (imageRegex.test(q.value.raw)) {
|
|
81
|
+
usedImages.push({
|
|
82
|
+
value: q.value.raw,
|
|
83
|
+
file,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
// String literals anywhere in code
|
|
92
|
+
StringLiteral(path) {
|
|
93
|
+
if (imageRegex.test(path.node.value)) {
|
|
94
|
+
usedImages.push({
|
|
95
|
+
value: path.node.value,
|
|
96
|
+
file,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
// Template literals: `/images/${name}.png`
|
|
102
|
+
TemplateLiteral(path) {
|
|
103
|
+
path.node.quasis.forEach((quasi) => {
|
|
104
|
+
if (imageRegex.test(quasi.value.raw)) {
|
|
105
|
+
usedImages.push({
|
|
106
|
+
value: quasi.value.raw,
|
|
107
|
+
file,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
// Arrays: ["img/a.png", "img/b.png"]
|
|
114
|
+
ArrayExpression(path) {
|
|
115
|
+
path.node.elements.forEach((el) => {
|
|
116
|
+
if (el?.type === "StringLiteral" && imageRegex.test(el.value)) {
|
|
117
|
+
usedImages.push({
|
|
118
|
+
value: el.value,
|
|
119
|
+
file,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
// Objects: { image: "img/a.png" }
|
|
126
|
+
ObjectProperty(path) {
|
|
127
|
+
const val = path.node.value;
|
|
128
|
+
if (val.type === "StringLiteral" && imageRegex.test(val.value)) {
|
|
129
|
+
usedImages.push({
|
|
130
|
+
value: val.value,
|
|
131
|
+
file,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const cssImages = await getCssImages(codeDirectory);
|
|
139
|
+
const combinedImages = [...usedImages, ...cssImages];
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
const resolver = createResolver(imageDirectory);
|
|
143
|
+
|
|
144
|
+
for (const file of files) {
|
|
145
|
+
let i = 0
|
|
146
|
+
let isFound = false;
|
|
147
|
+
while(!isFound && i < combinedImages.length) {
|
|
148
|
+
const imageFilePath = await resolver(path.resolve(combinedImages[i].file), combinedImages[i].value);
|
|
149
|
+
if(compareFiles(path.resolve(file), imageFilePath)) {
|
|
150
|
+
isFound = true;
|
|
151
|
+
break;
|
|
152
|
+
}else if(i === combinedImages.length - 1) {
|
|
153
|
+
if(options.excludeFilePrint && options.excludeFilePrint.length > 0) {
|
|
154
|
+
if(!isExcludedFile(file, options.excludeFilePrint)) {
|
|
155
|
+
unusedImages.push(file);
|
|
156
|
+
}
|
|
157
|
+
}else{
|
|
158
|
+
unusedImages.push(file);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
i++;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if(options.table) {
|
|
166
|
+
const table = new Table({
|
|
167
|
+
head: options.dryRun ? ['Unused Images (Would Delete)'] : ['Unused Images'],
|
|
168
|
+
colWidths: [50]
|
|
169
|
+
});
|
|
170
|
+
unusedImages.forEach(image => {
|
|
171
|
+
table.push([image]);
|
|
172
|
+
});
|
|
173
|
+
console.log(table.toString());
|
|
174
|
+
}else{
|
|
175
|
+
unusedImages.forEach(image => {
|
|
176
|
+
console.log(chalk.red(image));
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if(options.dryRun && unusedImages.length > 0) {
|
|
181
|
+
console.log(chalk.cyan(`\n[DRY RUN] Would delete ${unusedImages.length} file(s)`));
|
|
182
|
+
console.log(chalk.cyan('Run without --dry-run to actually delete files\n'));
|
|
183
|
+
} else if (!options.dryRun && unusedImages.length > 0) {
|
|
184
|
+
askDeleteFiles(unusedImages);
|
|
185
|
+
}
|
|
186
|
+
return unusedImages;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = {
|
|
190
|
+
getUnusedImages,
|
|
191
|
+
};
|
package/controllers/list.js
CHANGED
|
@@ -45,15 +45,6 @@ async function scan(chalk, path, options) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
async function image(chalk, path, options) {
|
|
49
|
-
const unusedImages = await unUsedImages(chalk,path, options);
|
|
50
|
-
if(options.table){
|
|
51
|
-
console.log(chalk.yellow('***************** Unused Images *****************'));
|
|
52
|
-
console.log(unusedImages.toString());
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
48
|
module.exports = {
|
|
58
49
|
list,
|
|
59
50
|
scan
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qleaner",
|
|
3
3
|
"packageManager": "yarn@4.6.0",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.18",
|
|
5
5
|
"main": "command.js",
|
|
6
6
|
"bin": "./bin/cli.js",
|
|
7
7
|
"scripts": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"commander": "^14.0.2",
|
|
21
21
|
"enhanced-resolve": "^5.18.3",
|
|
22
22
|
"fast-glob": "^3.3.3",
|
|
23
|
+
"ora": "^9.0.0",
|
|
23
24
|
"prompts": "^2.4.2"
|
|
24
25
|
}
|
|
25
26
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const fg = require('fast-glob');
|
|
3
|
+
const path = require('path');
|
|
3
4
|
|
|
4
|
-
function extractCssImages(cssContent) {
|
|
5
|
+
function extractCssImages(cssContent, images, file) {
|
|
5
6
|
const urlRegex = /url\((['"]?)(.*?)\1\)/g;
|
|
6
|
-
const images = new Set();
|
|
7
7
|
let match;
|
|
8
8
|
|
|
9
9
|
while ((match = urlRegex.exec(cssContent)) !== null) {
|
|
@@ -11,23 +11,28 @@ function extractCssImages(cssContent) {
|
|
|
11
11
|
|
|
12
12
|
// Only collect image file types
|
|
13
13
|
if (/\.(png|jpg|jpeg|svg|gif|webp)$/i.test(url)) {
|
|
14
|
-
images.
|
|
14
|
+
images.push({
|
|
15
|
+
value: url,
|
|
16
|
+
file: path.resolve(file),
|
|
17
|
+
});
|
|
15
18
|
}
|
|
16
19
|
}
|
|
17
|
-
|
|
18
|
-
return Array.from(images);
|
|
20
|
+
return images
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
async function getCssImages() {
|
|
23
|
+
async function getCssImages(directory = "src") {
|
|
22
24
|
const cssFiles = await fg([
|
|
23
|
-
|
|
25
|
+
`${directory}/**/*.{css,scss}`,
|
|
24
26
|
]);
|
|
27
|
+
let images = [];
|
|
25
28
|
for (const file of cssFiles) {
|
|
26
29
|
const css = fs.readFileSync(file, "utf-8");
|
|
27
|
-
|
|
28
|
-
console.log(images);
|
|
30
|
+
images = extractCssImages(css, images, file);
|
|
29
31
|
}
|
|
32
|
+
return images;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
|
|
35
|
+
module.exports = {
|
|
36
|
+
getCssImages,
|
|
37
|
+
};
|
|
33
38
|
|
package/utils/resolver.js
CHANGED
|
@@ -16,7 +16,7 @@ const resolver = create({
|
|
|
16
16
|
]
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
return function resolveImport(
|
|
19
|
+
return function resolveImport(sourceFile, importPath) {
|
|
20
20
|
// console.log(chalk.yellow('sourceFile--resolver'), sourceFile, chalk.yellow('importPath--resolver'), importPath);
|
|
21
21
|
return new Promise((resolve, reject) => {
|
|
22
22
|
resolver(path.dirname(sourceFile), importPath, (err, result) => {
|
package/utils/utils.js
CHANGED
|
@@ -53,4 +53,16 @@ async function deleteFiles(files) {
|
|
|
53
53
|
console.log(`Deleted ${files.length} files`);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
function isExcludedFile(file, excludeFiles) {
|
|
57
|
+
return excludeFiles.some((exclude) => file.includes(exclude));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function compareFiles(filePath, importPath) {
|
|
61
|
+
return filePath === importPath;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = {
|
|
65
|
+
askDeleteFiles,
|
|
66
|
+
isExcludedFile,
|
|
67
|
+
compareFiles,
|
|
68
|
+
};
|