qleaner 1.0.22 → 1.0.24

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