qleaner 1.0.15 โ 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/README.md +28 -11
- package/bin/cli.js +22 -7
- package/command.js +22 -22
- package/controllers/image.js +191 -0
- package/controllers/list.js +0 -9
- package/package.json +2 -1
- package/utils/cache.js +9 -1
- package/{cssImages.js โ utils/cssImages.js} +15 -10
- package/utils/resolver.js +1 -1
- package/utils/utils.js +13 -1
- package/unused-check-cache.json +0 -180
package/README.md
CHANGED
|
@@ -9,9 +9,10 @@ A powerful CLI tool to analyze and clean up your React codebase by finding unuse
|
|
|
9
9
|
- ๐ฏ **File listing**: List all files in your project
|
|
10
10
|
- ๐ **Table output**: Display results in formatted tables for better readability
|
|
11
11
|
- โ๏ธ **Flexible configuration**: Exclude directories and files from scanning
|
|
12
|
-
- ๐ **Fast performance**: Efficient file scanning across large codebases
|
|
12
|
+
- ๐ **Fast performance**: Efficient file scanning across large codebases with intelligent caching
|
|
13
13
|
- ๐ช **TypeScript support**: Works with TypeScript, JavaScript, JSX, and TSX files
|
|
14
14
|
- ๐งช **Dry run mode**: Preview what would be deleted without actually deleting files
|
|
15
|
+
- ๐พ **Smart caching**: Caches scan results for faster subsequent runs (automatically invalidates on file changes)
|
|
15
16
|
|
|
16
17
|
## Installation
|
|
17
18
|
|
|
@@ -80,6 +81,7 @@ qleaner qlean-scan <path> [options]
|
|
|
80
81
|
- `-F, --exclude-file-print <files...>` - Do not print the excluded files
|
|
81
82
|
- `-t, --table` - Display results in a formatted table
|
|
82
83
|
- `-d, --dry-run` - Show what would be deleted without actually deleting (skips prompt)
|
|
84
|
+
- `-C, --clear-cache` - Clear the cache before scanning (use when code changes have been made)
|
|
83
85
|
|
|
84
86
|
**Examples:**
|
|
85
87
|
|
|
@@ -107,6 +109,12 @@ qleaner qlean-scan src --dry-run
|
|
|
107
109
|
|
|
108
110
|
# Dry run with table output
|
|
109
111
|
qleaner qlean-scan src --dry-run --table
|
|
112
|
+
|
|
113
|
+
# Clear cache and scan (recommended after making code changes)
|
|
114
|
+
qleaner qlean-scan src --clear-cache
|
|
115
|
+
|
|
116
|
+
# Clear cache with dry run
|
|
117
|
+
qleaner qlean-scan src --clear-cache --dry-run
|
|
110
118
|
```
|
|
111
119
|
|
|
112
120
|
## Output Formats
|
|
@@ -127,11 +135,13 @@ Qleaner provides two output formats:
|
|
|
127
135
|
## How It Works
|
|
128
136
|
|
|
129
137
|
1. **File Discovery**: Recursively finds all `.tsx`, `.ts`, `.js`, and `.jsx` files in the specified directory
|
|
130
|
-
2. **
|
|
131
|
-
3. **
|
|
132
|
-
4. **
|
|
133
|
-
5. **
|
|
134
|
-
6. **
|
|
138
|
+
2. **Caching**: Checks cache for previously scanned files. Files that haven't changed are skipped for faster performance
|
|
139
|
+
3. **Import Extraction**: Parses files using Babel AST and extracts all import statements (only for changed files or cache misses)
|
|
140
|
+
4. **Module Resolution**: Uses enhanced-resolve to properly resolve import paths (supports path aliases like `@/` and `~/`)
|
|
141
|
+
5. **Analysis**: Compares file paths with resolved import paths to identify unused files
|
|
142
|
+
6. **Cache Update**: Saves scan results to cache for faster subsequent runs
|
|
143
|
+
7. **Reporting**: Outputs the results in standard or table format based on your preferences
|
|
144
|
+
8. **Safe Deletion**: In dry run mode, shows what would be deleted without making changes. In normal mode, prompts for confirmation before deletion
|
|
135
145
|
|
|
136
146
|
## Supported File Types
|
|
137
147
|
|
|
@@ -168,11 +178,12 @@ You can exclude directories and files from scanning using the command-line optio
|
|
|
168
178
|
|
|
169
179
|
1. **Start with a small scope**: Begin by scanning a specific directory before scanning the entire project
|
|
170
180
|
2. **Use dry run first**: Always run with `--dry-run` first to preview what would be deleted before actually deleting files
|
|
171
|
-
3. **
|
|
172
|
-
4. **
|
|
173
|
-
5. **
|
|
174
|
-
6. **
|
|
175
|
-
7. **
|
|
181
|
+
3. **Clear cache after code changes**: If you've made changes to your codebase (added/removed files, changed imports), use `--clear-cache` to ensure accurate results. The cache automatically invalidates when files change, but clearing it manually ensures a fresh scan
|
|
182
|
+
4. **Use exclusions**: Exclude test files and build outputs when scanning for unused files
|
|
183
|
+
5. **Review before deleting**: Always review the unused files list before removing them - some files might be used dynamically (e.g., through dynamic imports, configuration files, or asset references)
|
|
184
|
+
6. **Use table format**: The table format is easier to read for large results
|
|
185
|
+
7. **Combine options**: Use multiple flags together for comprehensive analysis
|
|
186
|
+
8. **Check dynamic imports**: Files imported using dynamic imports (`import()`) may appear as unused but are actually needed
|
|
176
187
|
|
|
177
188
|
## Important Notes
|
|
178
189
|
|
|
@@ -184,6 +195,12 @@ You can exclude directories and files from scanning using the command-line optio
|
|
|
184
195
|
|
|
185
196
|
๐ก **Tip**: Always use `--dry-run` first to preview what would be deleted. This is especially important in CI/CD pipelines or when scanning large codebases.
|
|
186
197
|
|
|
198
|
+
๐พ **Cache Management**: Qleaner uses intelligent caching to speed up scans. The cache automatically detects file changes via content hashing. However, if you've made significant changes to your codebase structure or want to ensure a completely fresh scan, use `--clear-cache` before scanning. This is particularly useful:
|
|
199
|
+
- After major refactoring
|
|
200
|
+
- When files have been moved or renamed
|
|
201
|
+
- When you want to ensure the most up-to-date results
|
|
202
|
+
- In CI/CD pipelines where you want consistent, fresh scans
|
|
203
|
+
|
|
187
204
|
## Requirements
|
|
188
205
|
|
|
189
206
|
- Node.js 14+
|
package/bin/cli.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
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;
|
|
@@ -46,23 +47,37 @@ async function loadChalk() {
|
|
|
46
47
|
)
|
|
47
48
|
.option("-t, --table", "Print the results in a table")
|
|
48
49
|
.option("-d, --dry-run", "Show what would be deleted without actually deleting (skips prompt)")
|
|
50
|
+
.option("-C, --clear-cache", "Clear the cache")
|
|
49
51
|
.action(async (path, options) => {
|
|
52
|
+
if(options.clearCache) {
|
|
53
|
+
clearCache(process.cwd());
|
|
54
|
+
console.log(chalk.green('โ Cache cleared successfully'));
|
|
55
|
+
}
|
|
50
56
|
await scan(chalk, path, options);
|
|
51
57
|
});
|
|
52
58
|
|
|
53
59
|
program
|
|
54
60
|
.command('qlean-image')
|
|
55
61
|
.description("Scan the project for unused images")
|
|
56
|
-
.argument("<
|
|
57
|
-
.
|
|
58
|
-
.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")
|
|
59
66
|
.option(
|
|
60
|
-
"-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...>",
|
|
61
74
|
"Scan but don't print the excluded files"
|
|
62
75
|
)
|
|
63
76
|
.option("-t, --table", "Print the results in a table")
|
|
64
|
-
.
|
|
65
|
-
|
|
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);
|
|
66
81
|
});
|
|
67
82
|
program.parse(process.argv);
|
|
68
83
|
})();
|
package/command.js
CHANGED
|
@@ -5,7 +5,13 @@ const parser = require("@babel/parser");
|
|
|
5
5
|
const traverse = require("@babel/traverse").default;
|
|
6
6
|
const path = require("path");
|
|
7
7
|
const { createResolver } = require("./utils/resolver");
|
|
8
|
-
const {
|
|
8
|
+
const {
|
|
9
|
+
loadCache,
|
|
10
|
+
getFileHash,
|
|
11
|
+
needsRebuild,
|
|
12
|
+
saveCache,
|
|
13
|
+
} = require("./utils/cache");
|
|
14
|
+
const { isExcludedFile, compareFiles } = require("./utils/utils");
|
|
9
15
|
|
|
10
16
|
async function getFiles(directory = "src", options, chalk) {
|
|
11
17
|
const contentPaths = [`${directory}/**/*.{tsx,ts,js,jsx}`];
|
|
@@ -101,7 +107,6 @@ async function getFiles(directory = "src", options, chalk) {
|
|
|
101
107
|
}
|
|
102
108
|
|
|
103
109
|
async function unUsedFiles(chalk, directory = "src", options) {
|
|
104
|
-
console.time('unUsedFiles');
|
|
105
110
|
const resolver = createResolver(directory);
|
|
106
111
|
const cache = loadCache(process.cwd());
|
|
107
112
|
const contentPaths = [`${directory}/**/*.{tsx,ts,js,jsx}`];
|
|
@@ -117,7 +122,7 @@ async function unUsedFiles(chalk, directory = "src", options) {
|
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
const files = await fg(contentPaths);
|
|
120
|
-
|
|
125
|
+
let imports = [];
|
|
121
126
|
const unusedFiles = [];
|
|
122
127
|
|
|
123
128
|
// debug log
|
|
@@ -125,7 +130,7 @@ async function unUsedFiles(chalk, directory = "src", options) {
|
|
|
125
130
|
// console.log('total files---', files.length);
|
|
126
131
|
for (const file of files) {
|
|
127
132
|
const code = fs.readFileSync(file, "utf8");
|
|
128
|
-
if(needsRebuild(file, code, cache)) {
|
|
133
|
+
if (needsRebuild(file, code, cache)) {
|
|
129
134
|
const ast = parser.parse(code, {
|
|
130
135
|
sourceType: "module",
|
|
131
136
|
plugins: ["jsx", "typescript"],
|
|
@@ -149,33 +154,32 @@ async function unUsedFiles(chalk, directory = "src", options) {
|
|
|
149
154
|
file: file,
|
|
150
155
|
line: node.loc.start.line,
|
|
151
156
|
column: node.loc.start.column,
|
|
152
|
-
lastModified: fs.statSync(file).mtime.getTime(),
|
|
157
|
+
lastModified: fs.statSync(path.resolve(file)).mtime.getTime(),
|
|
153
158
|
});
|
|
154
159
|
},
|
|
155
160
|
});
|
|
156
|
-
}else {
|
|
161
|
+
} else {
|
|
157
162
|
// debugCount++;
|
|
158
163
|
// console.log('cache hit', debugCount);
|
|
159
164
|
}
|
|
160
165
|
}
|
|
161
166
|
|
|
162
|
-
//
|
|
163
|
-
|
|
167
|
+
// imports beings empty shows all the files were cached
|
|
168
|
+
if(imports.length === 0) {
|
|
169
|
+
imports = cache[directory]? cache[directory].imports: [];
|
|
170
|
+
}
|
|
171
|
+
|
|
164
172
|
// debugCount = 0;
|
|
165
|
-
// console.log('total files', files.length);
|
|
166
173
|
for (const file of files) {
|
|
167
174
|
const code = fs.readFileSync(file, "utf8");
|
|
168
|
-
if(!cache[file].isImported || needsRebuild(file, code, cache)) {
|
|
175
|
+
if (!cache[file].isImported || needsRebuild(file, code, cache)) {
|
|
169
176
|
let i = 0;
|
|
170
|
-
// console.log('Checking', file);
|
|
171
177
|
let isFound = false;
|
|
172
178
|
while (!isFound && i < imports.length) {
|
|
173
179
|
const importFilePath = await resolver(
|
|
174
|
-
chalk,
|
|
175
180
|
imports[i].file,
|
|
176
181
|
imports[i].from
|
|
177
182
|
);
|
|
178
|
-
// console.log(chalk.blue('importFilePath'), importFilePath);
|
|
179
183
|
if (compareFiles(path.resolve(file), importFilePath)) {
|
|
180
184
|
isFound = true;
|
|
181
185
|
cache[file].isImported = true;
|
|
@@ -192,23 +196,19 @@ async function unUsedFiles(chalk, directory = "src", options) {
|
|
|
192
196
|
}
|
|
193
197
|
i++;
|
|
194
198
|
}
|
|
195
|
-
}else {
|
|
199
|
+
} else {
|
|
196
200
|
// debugCount++;
|
|
197
|
-
// console.log(
|
|
201
|
+
// console.log("debug hit", debugCount);
|
|
198
202
|
}
|
|
199
203
|
}
|
|
200
|
-
|
|
204
|
+
cache[directory] = {
|
|
205
|
+
imports: imports,
|
|
206
|
+
}
|
|
201
207
|
saveCache(process.cwd(), cache);
|
|
202
208
|
return unusedFiles;
|
|
203
209
|
}
|
|
204
210
|
|
|
205
|
-
function isExcludedFile(file, excludeFiles) {
|
|
206
|
-
return excludeFiles.some((exclude) => file.includes(exclude));
|
|
207
|
-
}
|
|
208
211
|
|
|
209
|
-
function compareFiles(filePath, importPath) {
|
|
210
|
-
return filePath === importPath;
|
|
211
|
-
}
|
|
212
212
|
|
|
213
213
|
module.exports = {
|
|
214
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.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
|
}
|
package/utils/cache.js
CHANGED
|
@@ -30,9 +30,17 @@ function saveCache(rootPath, cache) {
|
|
|
30
30
|
fs.writeFileSync(file, JSON.stringify(cache, null, 2))
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
|
|
34
|
+
function clearCache(rootPath) {
|
|
35
|
+
const file = path.join(rootPath, "unused-check-cache.json");
|
|
36
|
+
if (fs.existsSync(file)) {
|
|
37
|
+
fs.unlinkSync(file);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
33
40
|
module.exports = {
|
|
34
41
|
loadCache,
|
|
35
42
|
getFileHash,
|
|
36
43
|
needsRebuild,
|
|
37
|
-
saveCache
|
|
44
|
+
saveCache,
|
|
45
|
+
clearCache
|
|
38
46
|
}
|
|
@@ -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
|
+
};
|
package/unused-check-cache.json
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"src/hello.find.tsx": {
|
|
3
|
-
"hash": "13088ef1169a758d26b7fde99ca67d5a",
|
|
4
|
-
"imports": [
|
|
5
|
-
{
|
|
6
|
-
"from": "./demo/functions",
|
|
7
|
-
"file": "src/hello.find.tsx",
|
|
8
|
-
"line": 1,
|
|
9
|
-
"column": 0,
|
|
10
|
-
"lastModified": 1764776380790
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
"from": "~/demo/book/hidden",
|
|
14
|
-
"file": "src/hello.find.tsx",
|
|
15
|
-
"line": 2,
|
|
16
|
-
"column": 0,
|
|
17
|
-
"lastModified": 1764776380790
|
|
18
|
-
}
|
|
19
|
-
],
|
|
20
|
-
"isImported": true,
|
|
21
|
-
"lastModified": 1764776380790
|
|
22
|
-
},
|
|
23
|
-
"src/jambo.tsx": {
|
|
24
|
-
"hash": "6b9a7f99ad2bd68e567368a35bc2831c",
|
|
25
|
-
"imports": [
|
|
26
|
-
{
|
|
27
|
-
"from": "./demo/functions",
|
|
28
|
-
"file": "src/jambo.tsx",
|
|
29
|
-
"line": 1,
|
|
30
|
-
"column": 0,
|
|
31
|
-
"lastModified": 1764776380792
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
"from": "hello.find",
|
|
35
|
-
"file": "src/jambo.tsx",
|
|
36
|
-
"line": 2,
|
|
37
|
-
"column": 0,
|
|
38
|
-
"lastModified": 1764776380792
|
|
39
|
-
}
|
|
40
|
-
],
|
|
41
|
-
"isImported": true,
|
|
42
|
-
"lastModified": 1764776380792
|
|
43
|
-
},
|
|
44
|
-
"src/main.js": {
|
|
45
|
-
"hash": "961f4425237e1551bb8ec8a136f12a66",
|
|
46
|
-
"imports": [
|
|
47
|
-
{
|
|
48
|
-
"from": "jambo",
|
|
49
|
-
"file": "src/main.js",
|
|
50
|
-
"line": 1,
|
|
51
|
-
"column": 0,
|
|
52
|
-
"lastModified": 1764776380796
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
"from": "poa",
|
|
56
|
-
"file": "src/main.js",
|
|
57
|
-
"line": 2,
|
|
58
|
-
"column": 0,
|
|
59
|
-
"lastModified": 1764776380796
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
"from": "./style.css",
|
|
63
|
-
"file": "src/main.js",
|
|
64
|
-
"line": 3,
|
|
65
|
-
"column": 0,
|
|
66
|
-
"lastModified": 1764776380796
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
"from": "./demo/demo",
|
|
70
|
-
"file": "src/main.js",
|
|
71
|
-
"line": 4,
|
|
72
|
-
"column": 0,
|
|
73
|
-
"lastModified": 1764776380796
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
"from": "./jambo",
|
|
77
|
-
"file": "src/main.js",
|
|
78
|
-
"line": 5,
|
|
79
|
-
"column": 0,
|
|
80
|
-
"lastModified": 1764776380796
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
"from": "@/demo/book/hidden",
|
|
84
|
-
"file": "src/main.js",
|
|
85
|
-
"line": 6,
|
|
86
|
-
"column": 0,
|
|
87
|
-
"lastModified": 1764776380796
|
|
88
|
-
}
|
|
89
|
-
],
|
|
90
|
-
"isImported": true,
|
|
91
|
-
"lastModified": 1764776380796
|
|
92
|
-
},
|
|
93
|
-
"src/poa.tsx": {
|
|
94
|
-
"hash": "09d1c8072d500780c9d6b223f5609cc5",
|
|
95
|
-
"imports": [
|
|
96
|
-
{
|
|
97
|
-
"from": "one.png",
|
|
98
|
-
"file": "src/poa.tsx",
|
|
99
|
-
"line": 1,
|
|
100
|
-
"column": 0,
|
|
101
|
-
"lastModified": 1764776380809
|
|
102
|
-
}
|
|
103
|
-
],
|
|
104
|
-
"isImported": true,
|
|
105
|
-
"lastModified": 1764776380809
|
|
106
|
-
},
|
|
107
|
-
"src/demo/demo.tsx": {
|
|
108
|
-
"hash": "fe20d628c85aad8b491b3e45e6559ce4",
|
|
109
|
-
"imports": [
|
|
110
|
-
{
|
|
111
|
-
"from": "../hello.find",
|
|
112
|
-
"file": "src/demo/demo.tsx",
|
|
113
|
-
"line": 1,
|
|
114
|
-
"column": 0,
|
|
115
|
-
"lastModified": 1764828590237
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
"from": "../demo/book/hidden",
|
|
119
|
-
"file": "src/demo/demo.tsx",
|
|
120
|
-
"line": 2,
|
|
121
|
-
"column": 0,
|
|
122
|
-
"lastModified": 1764828590237
|
|
123
|
-
}
|
|
124
|
-
],
|
|
125
|
-
"isImported": true,
|
|
126
|
-
"lastModified": 1764828590237
|
|
127
|
-
},
|
|
128
|
-
"src/demo/functions.js": {
|
|
129
|
-
"hash": "091a10616a5be2b7c7803a3f7513453e",
|
|
130
|
-
"imports": [],
|
|
131
|
-
"isImported": true,
|
|
132
|
-
"lastModified": 1764776380788
|
|
133
|
-
},
|
|
134
|
-
"src/demo/public.tsx": {
|
|
135
|
-
"hash": "d41d8cd98f00b204e9800998ecf8427e",
|
|
136
|
-
"imports": [],
|
|
137
|
-
"isImported": false,
|
|
138
|
-
"lastModified": 1764690373726
|
|
139
|
-
},
|
|
140
|
-
"src/shell/main.js": {
|
|
141
|
-
"hash": "d41d8cd98f00b204e9800998ecf8427e",
|
|
142
|
-
"imports": [],
|
|
143
|
-
"isImported": false,
|
|
144
|
-
"lastModified": 1764822800241
|
|
145
|
-
},
|
|
146
|
-
"src/demo/book/hidden.tsx": {
|
|
147
|
-
"hash": "6c3fb783f6f246ad67252489ba9367f4",
|
|
148
|
-
"imports": [
|
|
149
|
-
{
|
|
150
|
-
"from": "../../main",
|
|
151
|
-
"file": "src/demo/book/hidden.tsx",
|
|
152
|
-
"line": 1,
|
|
153
|
-
"column": 0,
|
|
154
|
-
"lastModified": 1764776380785
|
|
155
|
-
}
|
|
156
|
-
],
|
|
157
|
-
"isImported": true,
|
|
158
|
-
"lastModified": 1764776380785
|
|
159
|
-
},
|
|
160
|
-
"src/demo/book/public.tsx": {
|
|
161
|
-
"hash": "d41d8cd98f00b204e9800998ecf8427e",
|
|
162
|
-
"imports": [],
|
|
163
|
-
"isImported": false,
|
|
164
|
-
"lastModified": 1764690329997
|
|
165
|
-
},
|
|
166
|
-
"src/layer/shell/shell.tsx": {
|
|
167
|
-
"hash": "659029bd48113a93f0ce44bc1e8005d3",
|
|
168
|
-
"imports": [
|
|
169
|
-
{
|
|
170
|
-
"from": "../../demo/book/hidden",
|
|
171
|
-
"file": "src/layer/shell/shell.tsx",
|
|
172
|
-
"line": 1,
|
|
173
|
-
"column": 0,
|
|
174
|
-
"lastModified": 1764776380795
|
|
175
|
-
}
|
|
176
|
-
],
|
|
177
|
-
"isImported": false,
|
|
178
|
-
"lastModified": 1764776380795
|
|
179
|
-
}
|
|
180
|
-
}
|