qleaner 1.0.21 → 1.0.23
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/command.js +46 -1
- package/controllers/image.js +264 -137
- package/controllers/list.js +1 -1
- package/package.json +2 -1
- package/utils/cssImages.js +6 -1
- package/utils/resolver.js +5 -1
package/command.js
CHANGED
|
@@ -4,6 +4,7 @@ const Table = require("cli-table3");
|
|
|
4
4
|
const parser = require("@babel/parser");
|
|
5
5
|
const traverse = require("@babel/traverse").default;
|
|
6
6
|
const path = require("path");
|
|
7
|
+
const cliProgress = require("cli-progress");
|
|
7
8
|
const { createResolver } = require("./utils/resolver");
|
|
8
9
|
const {
|
|
9
10
|
loadCache,
|
|
@@ -14,6 +15,14 @@ const {
|
|
|
14
15
|
const { isExcludedFile, compareFiles } = require("./utils/utils");
|
|
15
16
|
|
|
16
17
|
async function getFiles(directory = "src", options, chalk) {
|
|
18
|
+
const bars = new cliProgress.MultiBar({
|
|
19
|
+
clearOnComplete: false,
|
|
20
|
+
hideCursor: true,
|
|
21
|
+
format: chalk.cyan('{name}') +
|
|
22
|
+
' |' + '{bar}' + '| ' +
|
|
23
|
+
'{value}/{total} files || {status}'
|
|
24
|
+
}, cliProgress.Presets.shades_grey);
|
|
25
|
+
|
|
17
26
|
const contentPaths = [`${directory}/**/*.{tsx,ts,js,jsx}`];
|
|
18
27
|
if (options.excludeDir && options.excludeDir.length > 0) {
|
|
19
28
|
options.excludeDir.forEach((dir) => {
|
|
@@ -28,6 +37,12 @@ async function getFiles(directory = "src", options, chalk) {
|
|
|
28
37
|
|
|
29
38
|
const files = await fg(contentPaths);
|
|
30
39
|
const imports = [];
|
|
40
|
+
|
|
41
|
+
const scanBar = bars.create(files.length, 0, {
|
|
42
|
+
name: 'Scanning Files',
|
|
43
|
+
status: 'Scanning...',
|
|
44
|
+
});
|
|
45
|
+
|
|
31
46
|
for (const file of files) {
|
|
32
47
|
const code = fs.readFileSync(file, "utf8");
|
|
33
48
|
const ast = parser.parse(code, {
|
|
@@ -56,7 +71,10 @@ async function getFiles(directory = "src", options, chalk) {
|
|
|
56
71
|
// }
|
|
57
72
|
// },
|
|
58
73
|
// });
|
|
74
|
+
scanBar.increment();
|
|
59
75
|
}
|
|
76
|
+
|
|
77
|
+
bars.stop();
|
|
60
78
|
|
|
61
79
|
if (options.table) {
|
|
62
80
|
const tableImports = new Table({
|
|
@@ -107,6 +125,14 @@ async function getFiles(directory = "src", options, chalk) {
|
|
|
107
125
|
}
|
|
108
126
|
|
|
109
127
|
async function unUsedFiles(chalk, directory = "src", options) {
|
|
128
|
+
const bars = new cliProgress.MultiBar({
|
|
129
|
+
clearOnComplete: false,
|
|
130
|
+
hideCursor: true,
|
|
131
|
+
format: chalk.cyan('{name}') +
|
|
132
|
+
' |' + '{bar}' + '| ' +
|
|
133
|
+
'{value}/{total} files || {status}'
|
|
134
|
+
}, cliProgress.Presets.shades_grey);
|
|
135
|
+
|
|
110
136
|
const resolver = createResolver(directory);
|
|
111
137
|
const cache = loadCache(process.cwd());
|
|
112
138
|
const contentPaths = [`${directory}/**/*.{tsx,ts,js,jsx}`];
|
|
@@ -125,9 +151,14 @@ async function unUsedFiles(chalk, directory = "src", options) {
|
|
|
125
151
|
let imports = [];
|
|
126
152
|
const unusedFiles = [];
|
|
127
153
|
|
|
154
|
+
const scanBar = bars.create(files.length, 0, {
|
|
155
|
+
name: 'Scanning Files',
|
|
156
|
+
status: 'Scanning...',
|
|
157
|
+
});
|
|
158
|
+
|
|
128
159
|
// debug log
|
|
129
160
|
// let debugCount = 0;
|
|
130
|
-
|
|
161
|
+
let i = 0;
|
|
131
162
|
for (const file of files) {
|
|
132
163
|
const code = fs.readFileSync(file, "utf8");
|
|
133
164
|
if (needsRebuild(file, code, cache)) {
|
|
@@ -162,6 +193,9 @@ async function unUsedFiles(chalk, directory = "src", options) {
|
|
|
162
193
|
// debugCount++;
|
|
163
194
|
// console.log('cache hit', debugCount);
|
|
164
195
|
}
|
|
196
|
+
scanBar.update(++i, {
|
|
197
|
+
status: 'Scanning...',
|
|
198
|
+
});
|
|
165
199
|
}
|
|
166
200
|
|
|
167
201
|
// imports beings empty shows all the files were cached
|
|
@@ -169,7 +203,13 @@ async function unUsedFiles(chalk, directory = "src", options) {
|
|
|
169
203
|
imports = cache[directory]? cache[directory].imports: [];
|
|
170
204
|
}
|
|
171
205
|
|
|
206
|
+
const checkBar = bars.create(files.length, 0, {
|
|
207
|
+
name: 'Checking Usage',
|
|
208
|
+
status: 'Checking...',
|
|
209
|
+
});
|
|
210
|
+
|
|
172
211
|
// debugCount = 0;
|
|
212
|
+
i = 0;
|
|
173
213
|
for (const file of files) {
|
|
174
214
|
const code = fs.readFileSync(file, "utf8");
|
|
175
215
|
if (!cache[file].isImported || needsRebuild(file, code, cache)) {
|
|
@@ -200,7 +240,12 @@ async function unUsedFiles(chalk, directory = "src", options) {
|
|
|
200
240
|
// debugCount++;
|
|
201
241
|
// console.log("debug hit", debugCount);
|
|
202
242
|
}
|
|
243
|
+
checkBar.update(++i, {
|
|
244
|
+
status: 'Checking...',
|
|
245
|
+
});
|
|
203
246
|
}
|
|
247
|
+
|
|
248
|
+
bars.stop();
|
|
204
249
|
cache[directory] = {
|
|
205
250
|
imports: imports,
|
|
206
251
|
}
|
package/controllers/image.js
CHANGED
|
@@ -1,189 +1,316 @@
|
|
|
1
1
|
const fg = require("fast-glob");
|
|
2
2
|
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
3
4
|
const parser = require("@babel/parser");
|
|
4
5
|
const traverse = require("@babel/traverse").default;
|
|
6
|
+
const Table = require("cli-table3");
|
|
5
7
|
const { getCssImages } = require("../utils/cssImages");
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const { compareFiles, isExcludedFile, askDeleteFiles } = require("../utils/utils");
|
|
9
|
-
const Table = require('cli-table3');
|
|
8
|
+
const { askDeleteFiles } = require("../utils/utils");
|
|
9
|
+
const cliProgress = require("cli-progress");
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
}
|
|
11
|
+
// Regex for matching url("x.png") or url('x.png') or bg-[url('x.png')]
|
|
12
|
+
const URL_EXTRACT_REGEX = /url\((['"]?)([^"')]+)\1\)/gi;
|
|
26
13
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
14
|
+
// Normal image file regex
|
|
15
|
+
const IMAGE_REGEX = /\.(png|jpe?g|svg|gif|webp)$/i;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Normalize image paths for consistent matching.
|
|
19
|
+
*/
|
|
20
|
+
function normalize(value, imageDirectory) {
|
|
21
|
+
if (!value) return null;
|
|
22
|
+
|
|
23
|
+
// Extract url() paths
|
|
24
|
+
const match = [...value.matchAll(URL_EXTRACT_REGEX)];
|
|
25
|
+
if (match.length > 0) {
|
|
26
|
+
value = match[0][2];
|
|
32
27
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
|
|
29
|
+
// Remove query params
|
|
30
|
+
value = value.split("?")[0];
|
|
31
|
+
|
|
32
|
+
// Convert leading "/" to relative project path
|
|
33
|
+
if (value.startsWith("/")) {
|
|
34
|
+
return path.resolve(path.join(imageDirectory, value.slice(1)));
|
|
37
35
|
}
|
|
38
36
|
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
// Resolve relative paths
|
|
38
|
+
return path.resolve(path.join(imageDirectory, value));
|
|
39
|
+
}
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Extracts all strings in a tagged template literal (styled-components, css``)
|
|
43
|
+
*/
|
|
44
|
+
function extractFromTemplateLiteral(quasis) {
|
|
45
|
+
const results = [];
|
|
46
|
+
quasis.forEach((q) => {
|
|
47
|
+
const matches = [...q.value.raw.matchAll(URL_EXTRACT_REGEX)];
|
|
48
|
+
for (const m of matches) {
|
|
49
|
+
results.push(m[2]);
|
|
50
|
+
}
|
|
51
|
+
if (IMAGE_REGEX.test(q.value.raw)) {
|
|
52
|
+
results.push(q.value.raw);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return results;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Extract images from JSX style object: style={{ backgroundImage: "url('/img/a.png')" }}
|
|
60
|
+
*/
|
|
61
|
+
function extractFromJSXStyle(node, file, collected) {
|
|
62
|
+
if (!node || node.type !== "JSXExpressionContainer") return;
|
|
63
|
+
|
|
64
|
+
const expr = node.expression;
|
|
65
|
+
if (!expr || expr.type !== "ObjectExpression") return;
|
|
66
|
+
|
|
67
|
+
expr.properties.forEach((prop) => {
|
|
68
|
+
if (
|
|
69
|
+
prop.type !== "ObjectProperty" ||
|
|
70
|
+
!prop.key ||
|
|
71
|
+
!prop.value
|
|
72
|
+
) return;
|
|
73
|
+
|
|
74
|
+
const keyName = prop.key.name || prop.key.value;
|
|
75
|
+
|
|
76
|
+
if (!keyName) return;
|
|
77
|
+
|
|
78
|
+
const isImageField =
|
|
79
|
+
keyName.toLowerCase().includes("background") ||
|
|
80
|
+
keyName.toLowerCase().includes("image") ||
|
|
81
|
+
keyName.toLowerCase().includes("mask");
|
|
82
|
+
|
|
83
|
+
if (!isImageField) return;
|
|
84
|
+
|
|
85
|
+
// String literal
|
|
86
|
+
if (prop.value.type === "StringLiteral") {
|
|
87
|
+
const raw = prop.value.value;
|
|
88
|
+
const matches = [...raw.matchAll(URL_EXTRACT_REGEX)];
|
|
89
|
+
if (matches.length > 0) {
|
|
90
|
+
matches.forEach((m) =>
|
|
91
|
+
collected.add(JSON.stringify({ path: m[2], file }))
|
|
92
|
+
);
|
|
93
|
+
} else if (IMAGE_REGEX.test(raw)) {
|
|
94
|
+
collected.add(JSON.stringify({ path: raw, file }));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Template literal
|
|
99
|
+
if (prop.value.type === "TemplateLiteral") {
|
|
100
|
+
extractFromTemplateLiteral(prop.value.quasis).forEach((v) => {
|
|
101
|
+
collected.add(JSON.stringify({ path: v, file }));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* MAIN FUNCTION
|
|
109
|
+
*/
|
|
110
|
+
async function getUnusedImages(chalk, imageDirectory, codeDirectory, options) {
|
|
111
|
+
const bars = new cliProgress.MultiBar({
|
|
112
|
+
clearOnComplete: false,
|
|
113
|
+
hideCursor: true,
|
|
114
|
+
format: chalk.cyan('{name}') +
|
|
115
|
+
' |' + '{bar}' + '| ' +
|
|
116
|
+
'{value}/{total} files || {status}'
|
|
117
|
+
}, cliProgress.Presets.shades_grey)
|
|
118
|
+
|
|
119
|
+
const used = new Set();
|
|
120
|
+
const unusedImages = [];
|
|
121
|
+
|
|
122
|
+
// ---- Collect image files in asset directory ----
|
|
123
|
+
const imageFiles = await fg([`${imageDirectory}/**/*.{png,jpg,jpeg,svg,gif,webp}`]);
|
|
43
124
|
|
|
125
|
+
// ---- Scan Code Files ----
|
|
126
|
+
const codeFiles = await fg([`${codeDirectory}/**/*.{js,jsx,ts,tsx}`]);
|
|
127
|
+
const codeBar = bars.create(codeFiles.length, 0, {
|
|
128
|
+
name: 'Code Files',
|
|
129
|
+
status: 'Scanning...',
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
for (const file of codeFiles) {
|
|
44
134
|
const code = fs.readFileSync(file, "utf8");
|
|
135
|
+
|
|
45
136
|
const ast = parser.parse(code, {
|
|
46
137
|
sourceType: "module",
|
|
47
138
|
plugins: ["jsx", "typescript"],
|
|
48
139
|
});
|
|
49
140
|
|
|
50
141
|
traverse(ast, {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
142
|
+
/**
|
|
143
|
+
* import logo from './img/a.png'
|
|
144
|
+
*/
|
|
145
|
+
ImportDeclaration(pathNode) {
|
|
146
|
+
const val = pathNode.node.source.value;
|
|
147
|
+
if (IMAGE_REGEX.test(val)) {
|
|
148
|
+
used.add(JSON.stringify({ path: val, file }));
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* require('./img/a.png')
|
|
154
|
+
*/
|
|
155
|
+
CallExpression(pathNode) {
|
|
156
|
+
const callee = pathNode.node.callee;
|
|
157
|
+
const args = pathNode.node.arguments;
|
|
158
|
+
|
|
159
|
+
if (
|
|
160
|
+
callee.type === "Identifier" &&
|
|
161
|
+
callee.name === "require" &&
|
|
162
|
+
args.length &&
|
|
163
|
+
args[0].type === "StringLiteral"
|
|
164
|
+
) {
|
|
165
|
+
const val = args[0].value;
|
|
166
|
+
if (IMAGE_REGEX.test(val)) {
|
|
167
|
+
used.add(JSON.stringify({ path: val, file }));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// dynamic import("./a.png")
|
|
172
|
+
if (
|
|
173
|
+
callee.type === "Import" &&
|
|
174
|
+
args.length &&
|
|
175
|
+
args[0].type === "StringLiteral"
|
|
176
|
+
) {
|
|
177
|
+
const val = args[0].value;
|
|
178
|
+
if (IMAGE_REGEX.test(val)) {
|
|
179
|
+
used.add(JSON.stringify({ path: val, file }));
|
|
64
180
|
}
|
|
65
181
|
}
|
|
182
|
+
},
|
|
66
183
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
184
|
+
/**
|
|
185
|
+
* <img src="...">
|
|
186
|
+
*/
|
|
187
|
+
JSXAttribute(attr) {
|
|
188
|
+
if (attr.node.name.name === "src") {
|
|
189
|
+
const v = attr.node.value;
|
|
190
|
+
if (!v) return;
|
|
191
|
+
|
|
192
|
+
if (v.type === "StringLiteral") {
|
|
193
|
+
if (IMAGE_REGEX.test(v.value)) {
|
|
194
|
+
used.add(JSON.stringify({ path: v.value, file }));
|
|
195
|
+
}
|
|
75
196
|
}
|
|
76
197
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
file,
|
|
84
|
-
});
|
|
198
|
+
if (v.type === "JSXExpressionContainer") {
|
|
199
|
+
const expr = v.expression;
|
|
200
|
+
|
|
201
|
+
if (expr.type === "StringLiteral") {
|
|
202
|
+
if (IMAGE_REGEX.test(expr.value)) {
|
|
203
|
+
used.add(JSON.stringify({ path: expr.value, file }));
|
|
85
204
|
}
|
|
86
|
-
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (expr.type === "TemplateLiteral") {
|
|
208
|
+
extractFromTemplateLiteral(expr.quasis).forEach((v) => {
|
|
209
|
+
used.add(JSON.stringify({ path: v, file }));
|
|
210
|
+
});
|
|
211
|
+
}
|
|
87
212
|
}
|
|
88
213
|
}
|
|
89
|
-
},
|
|
90
214
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
usedImages.push({
|
|
95
|
-
value: path.node.value,
|
|
96
|
-
file,
|
|
97
|
-
});
|
|
215
|
+
// Handle style={{ backgroundImage: "url(...)" }}
|
|
216
|
+
if (attr.node.name.name === "style") {
|
|
217
|
+
extractFromJSXStyle(attr.node.value, file, used);
|
|
98
218
|
}
|
|
99
219
|
},
|
|
100
220
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
221
|
+
/**
|
|
222
|
+
* String Literals anywhere
|
|
223
|
+
*/
|
|
224
|
+
StringLiteral(p) {
|
|
225
|
+
const val = p.node.value;
|
|
226
|
+
|
|
227
|
+
if (IMAGE_REGEX.test(val)) {
|
|
228
|
+
used.add(JSON.stringify({ path: val, file }));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// detect url("...") inside strings (e.g., Tailwind)
|
|
232
|
+
const matches = [...val.matchAll(URL_EXTRACT_REGEX)];
|
|
233
|
+
matches.forEach((m) =>
|
|
234
|
+
used.add(JSON.stringify({ path: m[2], file }))
|
|
235
|
+
);
|
|
111
236
|
},
|
|
112
237
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
file,
|
|
120
|
-
});
|
|
121
|
-
}
|
|
238
|
+
/**
|
|
239
|
+
* Template Literals anywhere
|
|
240
|
+
*/
|
|
241
|
+
TemplateLiteral(p) {
|
|
242
|
+
extractFromTemplateLiteral(p.node.quasis).forEach((v) => {
|
|
243
|
+
used.add(JSON.stringify({ path: v, file }));
|
|
122
244
|
});
|
|
123
245
|
},
|
|
124
246
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
});
|
|
133
|
-
}
|
|
247
|
+
/**
|
|
248
|
+
* styled-components & css`` blocks
|
|
249
|
+
*/
|
|
250
|
+
TaggedTemplateExpression(p) {
|
|
251
|
+
const quasi = p.node.quasi;
|
|
252
|
+
const extracted = extractFromTemplateLiteral(quasi.quasis);
|
|
253
|
+
extracted.forEach((v) => used.add(JSON.stringify({ path: v, file })));
|
|
134
254
|
},
|
|
135
255
|
});
|
|
256
|
+
codeBar.increment();
|
|
136
257
|
}
|
|
137
258
|
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if(!isExcludedFile(filePath, options.excludeFilePrint)) {
|
|
156
|
-
unusedImages.push(filePath);
|
|
157
|
-
}
|
|
158
|
-
}else{
|
|
159
|
-
unusedImages.push(filePath);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
i++;
|
|
163
|
-
}
|
|
259
|
+
// Add CSS images
|
|
260
|
+
const cssImages = await getCssImages(codeDirectory, bars);
|
|
261
|
+
cssImages.forEach((img) =>
|
|
262
|
+
used.add(JSON.stringify({ path: img.value, file: img.file }))
|
|
263
|
+
);
|
|
264
|
+
const normalizeBar = bars.create(used.size, 0, {
|
|
265
|
+
name: 'Normalizing Images',
|
|
266
|
+
status: 'Normalizing...',
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Normalize all used images
|
|
270
|
+
const normalizedUsed = new Set();
|
|
271
|
+
for (const entry of used) {
|
|
272
|
+
normalizeBar.increment();
|
|
273
|
+
const { path: p } = JSON.parse(entry);
|
|
274
|
+
const normalized = normalize(p, imageDirectory);
|
|
275
|
+
if (normalized) normalizedUsed.add(normalized);
|
|
164
276
|
}
|
|
165
277
|
|
|
166
|
-
|
|
278
|
+
const unusedBar = bars.create(imageFiles.length, 0, {
|
|
279
|
+
name: 'Determining Unused Images',
|
|
280
|
+
status: 'Determining...',
|
|
281
|
+
});
|
|
282
|
+
// ---- Determine unused ----
|
|
283
|
+
for (const img of imageFiles) {
|
|
284
|
+
unusedBar.increment();
|
|
285
|
+
const full = path.resolve(img);
|
|
286
|
+
if (!normalizedUsed.has(full)) {
|
|
287
|
+
unusedImages.push(full);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
bars.stop();
|
|
291
|
+
// ---- Output table or list ----
|
|
292
|
+
if (options.table) {
|
|
167
293
|
const table = new Table({
|
|
168
|
-
head: options.dryRun ? [
|
|
169
|
-
colWidths: [
|
|
170
|
-
});
|
|
171
|
-
unusedImages.forEach(image => {
|
|
172
|
-
table.push([image]);
|
|
294
|
+
head: options.dryRun ? ["Unused Images (Would Delete)"] : ["Unused Images"],
|
|
295
|
+
colWidths: [100],
|
|
173
296
|
});
|
|
297
|
+
unusedImages.forEach((img) => table.push([img]));
|
|
174
298
|
console.log(table.toString());
|
|
175
|
-
}else{
|
|
176
|
-
unusedImages.forEach(
|
|
177
|
-
console.log(chalk.red(image));
|
|
178
|
-
});
|
|
299
|
+
} else {
|
|
300
|
+
unusedImages.forEach((img) => console.log(chalk.red(img)));
|
|
179
301
|
}
|
|
180
302
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
console.log(
|
|
184
|
-
|
|
303
|
+
// ---- deletion logic ----
|
|
304
|
+
if (options.dryRun) {
|
|
305
|
+
console.log(
|
|
306
|
+
chalk.cyan(
|
|
307
|
+
`\n[DRY RUN] Would delete ${unusedImages.length} file(s)`
|
|
308
|
+
)
|
|
309
|
+
);
|
|
310
|
+
} else if (unusedImages.length > 0) {
|
|
185
311
|
askDeleteFiles(unusedImages);
|
|
186
312
|
}
|
|
313
|
+
|
|
187
314
|
return unusedImages;
|
|
188
315
|
}
|
|
189
316
|
|
package/controllers/list.js
CHANGED
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.23",
|
|
5
5
|
"main": "command.js",
|
|
6
6
|
"bin": "./bin/cli.js",
|
|
7
7
|
"scripts": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"@babel/parser": "^7.28.5",
|
|
17
17
|
"@babel/traverse": "^7.28.5",
|
|
18
18
|
"chalk": "^5.6.2",
|
|
19
|
+
"cli-progress": "^3.12.0",
|
|
19
20
|
"cli-table3": "^0.6.5",
|
|
20
21
|
"commander": "^14.0.2",
|
|
21
22
|
"enhanced-resolve": "^5.18.3",
|
package/utils/cssImages.js
CHANGED
|
@@ -20,12 +20,17 @@ function extractCssImages(cssContent, images, file) {
|
|
|
20
20
|
return images
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
async function getCssImages(directory = "src") {
|
|
23
|
+
async function getCssImages(directory = "src", bars) {
|
|
24
24
|
const cssFiles = await fg([
|
|
25
25
|
`${directory}/**/*.{css,scss}`,
|
|
26
26
|
]);
|
|
27
27
|
let images = [];
|
|
28
|
+
const cssBar = bars.create(cssFiles.length, 0, {
|
|
29
|
+
name: 'CSS Files',
|
|
30
|
+
status: 'Scanning...',
|
|
31
|
+
});
|
|
28
32
|
for (const file of cssFiles) {
|
|
33
|
+
cssBar.increment();
|
|
29
34
|
const css = fs.readFileSync(file, "utf-8");
|
|
30
35
|
images = extractCssImages(css, images, file);
|
|
31
36
|
}
|
package/utils/resolver.js
CHANGED