qleaner 1.0.16 → 1.0.17

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 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("<path>", "The path to the directory to scan for unused images")
62
- .option("-e, --exclude-dir <dir...>", "Exclude directories from the scan")
63
- .option("-f, --exclude-file <file...>", "Exclude files from the scan")
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
- .action(async (path, options) => {
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(ora, 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
+ };
@@ -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.16",
4
+ "version": "1.0.17",
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.add(url);
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
- "src/**/*.{css,scss}",
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
- const images = extractCssImages(css);
28
- console.log(images);
30
+ images = extractCssImages(css, images, file);
29
31
  }
32
+ return images;
30
33
  }
31
34
 
32
- getCssImages();
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(chalk,sourceFile, importPath) {
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
- module.exports = askDeleteFiles;
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
+ };