qleaner 1.0.10 → 1.0.12
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 +61 -6
- package/bin/cli.js +21 -27
- package/command.js +152 -109
- package/controllers/list.js +60 -0
- package/package.json +6 -3
- package/unused-check-cache.json +180 -0
- package/utils/cache.js +38 -0
- package/utils/resolver.js +32 -0
- package/utils/utils.js +56 -0
package/README.md
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# Qleaner
|
|
2
2
|
|
|
3
|
-
A powerful CLI tool to analyze and clean up your React codebase by finding unused files and listing all imports.
|
|
3
|
+
A powerful CLI tool to analyze and clean up your React codebase by finding unused files and listing all imports.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- 🔍 **Scan for unused files**: Identify files that are not imported anywhere in your project
|
|
8
|
-
- 📋 **List all imports**: Get a complete list of all import statements in your codebase
|
|
8
|
+
- 📋 **List all imports**: Get a complete list of all import statements in your codebase with file locations
|
|
9
9
|
- 🎯 **File listing**: List all files in your project
|
|
10
|
+
- 📊 **Table output**: Display results in formatted tables for better readability
|
|
10
11
|
- ⚙️ **Flexible configuration**: Exclude directories and files from scanning
|
|
11
|
-
- 🚀 **Fast performance**:
|
|
12
|
+
- 🚀 **Fast performance**: Efficient file scanning across large codebases
|
|
12
13
|
- 💪 **TypeScript support**: Works with TypeScript, JavaScript, JSX, and TSX files
|
|
13
14
|
|
|
14
15
|
## Installation
|
|
@@ -40,6 +41,7 @@ qleaner qlean-list <path> [options]
|
|
|
40
41
|
- `-e, --exclude-dir <dir...>` - Exclude directories from the scan
|
|
41
42
|
- `-f, --exclude-file <file...>` - Exclude files from the scan
|
|
42
43
|
- `-F, --exclude-file-print <file...>` - Do not print the excluded files
|
|
44
|
+
- `-t, --table` - Display results in a formatted table
|
|
43
45
|
|
|
44
46
|
**Examples:**
|
|
45
47
|
|
|
@@ -50,11 +52,17 @@ qleaner qlean-list src --list-files
|
|
|
50
52
|
# List all imports in src directory
|
|
51
53
|
qleaner qlean-list src --list-imports
|
|
52
54
|
|
|
55
|
+
# List both files and imports in table format
|
|
56
|
+
qleaner qlean-list src --list-files --list-imports --table
|
|
57
|
+
|
|
53
58
|
# List files excluding node_modules and dist
|
|
54
59
|
qleaner qlean-list src --list-files -e node_modules dist
|
|
55
60
|
|
|
56
61
|
# List imports excluding specific files
|
|
57
62
|
qleaner qlean-list src --list-imports -f "**/*.test.js" "**/*.spec.js"
|
|
63
|
+
|
|
64
|
+
# List with table output
|
|
65
|
+
qleaner qlean-list src --list-files --list-imports --table
|
|
58
66
|
```
|
|
59
67
|
|
|
60
68
|
### Scan for Unused Files
|
|
@@ -69,6 +77,7 @@ qleaner qlean-scan <path> [options]
|
|
|
69
77
|
- `-e, --exclude-dir <dir...>` - Exclude directories from the scan
|
|
70
78
|
- `-f, --exclude-file <file...>` - Exclude files from the scan
|
|
71
79
|
- `-F, --exclude-file-print <files...>` - Do not print the excluded files
|
|
80
|
+
- `-t, --table` - Display results in a formatted table
|
|
72
81
|
|
|
73
82
|
**Examples:**
|
|
74
83
|
|
|
@@ -76,6 +85,9 @@ qleaner qlean-scan <path> [options]
|
|
|
76
85
|
# Scan src directory for unused files
|
|
77
86
|
qleaner qlean-scan src
|
|
78
87
|
|
|
88
|
+
# Display unused files in a table format
|
|
89
|
+
qleaner qlean-scan src --table
|
|
90
|
+
|
|
79
91
|
# Scan excluding test directories
|
|
80
92
|
qleaner qlean-scan src -e __tests__ __mocks__ test
|
|
81
93
|
|
|
@@ -84,14 +96,31 @@ qleaner qlean-scan src -f "**/*.test.js" "**/*.stories.js"
|
|
|
84
96
|
|
|
85
97
|
# Scan with multiple exclusions
|
|
86
98
|
qleaner qlean-scan src -e node_modules dist -f "**/*.config.js"
|
|
99
|
+
|
|
100
|
+
# Scan with table output and exclusions
|
|
101
|
+
qleaner qlean-scan src --table -e __tests__ dist -f "**/*.config.js"
|
|
87
102
|
```
|
|
88
103
|
|
|
104
|
+
## Output Formats
|
|
105
|
+
|
|
106
|
+
Qleaner provides two output formats:
|
|
107
|
+
|
|
108
|
+
1. **Standard output**: Color-coded text output
|
|
109
|
+
- Green for files
|
|
110
|
+
- Yellow for imports
|
|
111
|
+
- Red for unused files
|
|
112
|
+
|
|
113
|
+
2. **Table output**: Formatted tables with organized columns (use `--table` flag)
|
|
114
|
+
- Import tables show: File, Line, Column, and Import path
|
|
115
|
+
- File tables show: File path
|
|
116
|
+
- Unused files table shows: Unused file paths
|
|
117
|
+
|
|
89
118
|
## How It Works
|
|
90
119
|
|
|
91
|
-
1. **File Discovery**:
|
|
92
|
-
2. **
|
|
120
|
+
1. **File Discovery**: Recursively finds all `.tsx`, `.ts`, `.js`, and `.jsx` files in the specified directory
|
|
121
|
+
2. **Import Extraction**: Parses files and extracts all import statements
|
|
93
122
|
3. **Analysis**: Compares file paths with import paths to identify unused files
|
|
94
|
-
4. **Reporting**: Outputs the results
|
|
123
|
+
4. **Reporting**: Outputs the results in standard or table format based on your preferences
|
|
95
124
|
|
|
96
125
|
## Supported File Types
|
|
97
126
|
|
|
@@ -106,6 +135,7 @@ qleaner qlean-scan src -e node_modules dist -f "**/*.config.js"
|
|
|
106
135
|
- 📊 **Code analysis**: Understand import patterns and dependencies in your codebase
|
|
107
136
|
- 🔍 **Project audit**: Identify orphaned files that may have been forgotten
|
|
108
137
|
- 📦 **Bundle optimization**: Find files that can be removed to reduce bundle size
|
|
138
|
+
- 🎯 **Maintenance**: Keep your codebase clean and maintainable
|
|
109
139
|
|
|
110
140
|
## Configuration
|
|
111
141
|
|
|
@@ -115,6 +145,31 @@ You can exclude directories and files from scanning using the command-line optio
|
|
|
115
145
|
- Excluding third-party libraries
|
|
116
146
|
- Excluding configuration files
|
|
117
147
|
|
|
148
|
+
**Common exclusions:**
|
|
149
|
+
- Test files: `-f "**/*.test.*" "**/*.spec.*"`
|
|
150
|
+
- Storybook files: `-f "**/*.stories.*"`
|
|
151
|
+
- Test directories: `-e __tests__ __mocks__ test`
|
|
152
|
+
- Build outputs: `-e dist build .next`
|
|
153
|
+
- Configuration files: `-f "**/*.config.*"`
|
|
154
|
+
- Third-party code: `-e node_modules vendor`
|
|
155
|
+
|
|
156
|
+
## Tips and Best Practices
|
|
157
|
+
|
|
158
|
+
1. **Start with a small scope**: Begin by scanning a specific directory before scanning the entire project
|
|
159
|
+
2. **Use exclusions**: Exclude test files and build outputs when scanning for unused files
|
|
160
|
+
3. **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)
|
|
161
|
+
4. **Use table format**: The table format is easier to read for large results
|
|
162
|
+
5. **Combine options**: Use multiple flags together for comprehensive analysis
|
|
163
|
+
6. **Check dynamic imports**: Files imported using dynamic imports (`import()`) may appear as unused but are actually needed
|
|
164
|
+
|
|
165
|
+
## Important Notes
|
|
166
|
+
|
|
167
|
+
⚠️ **Warning**: Always review files before deletion. Some files might be:
|
|
168
|
+
- Used dynamically (dynamic imports)
|
|
169
|
+
- Referenced in configuration files
|
|
170
|
+
- Required for build processes
|
|
171
|
+
- Used as entry points that aren't directly imported
|
|
172
|
+
|
|
118
173
|
## Requirements
|
|
119
174
|
|
|
120
175
|
- Node.js 14+
|
package/bin/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
const { Command } = require("commander");
|
|
3
|
-
const {
|
|
4
|
-
|
|
3
|
+
const { list, scan } = require("../controllers/list");
|
|
4
|
+
|
|
5
5
|
|
|
6
6
|
async function loadChalk() {
|
|
7
7
|
return (await import("chalk")).default;
|
|
@@ -31,13 +31,7 @@ async function loadChalk() {
|
|
|
31
31
|
)
|
|
32
32
|
.option("-t, --table", "Print the results in a table")
|
|
33
33
|
.action(async (path, options) => {
|
|
34
|
-
|
|
35
|
-
if(options.table){
|
|
36
|
-
console.log(chalk.yellow('***************** Imported Files *****************'));
|
|
37
|
-
console.log(tableImports.toString());
|
|
38
|
-
console.log(chalk.green('***************** List Files *****************'));
|
|
39
|
-
console.log(tableFiles.toString());
|
|
40
|
-
}
|
|
34
|
+
await list(chalk, path, options);
|
|
41
35
|
});
|
|
42
36
|
|
|
43
37
|
program
|
|
@@ -48,27 +42,27 @@ async function loadChalk() {
|
|
|
48
42
|
.option("-f, --exclude-file <file...>", "Exclude files from the scan")
|
|
49
43
|
.option(
|
|
50
44
|
"-F, --exclude-file-print <files...>",
|
|
51
|
-
"
|
|
45
|
+
"Scan but don't print the excluded files"
|
|
52
46
|
)
|
|
53
47
|
.option("-t, --table", "Print the results in a table")
|
|
48
|
+
.option("-d, --dry-run", "Show what would be deleted without actually deleting (skips prompt)")
|
|
54
49
|
.action(async (path, options) => {
|
|
55
|
-
|
|
56
|
-
// console.clear()
|
|
57
|
-
if(options.table){
|
|
58
|
-
const table = new Table({
|
|
59
|
-
head: ['Unused Files'],
|
|
60
|
-
colWidths: [50]
|
|
61
|
-
})
|
|
62
|
-
unusedFiles.forEach(file => {
|
|
63
|
-
table.push([file])
|
|
64
|
-
})
|
|
65
|
-
console.log(table.toString())
|
|
66
|
-
|
|
67
|
-
}else {
|
|
68
|
-
unusedFiles.forEach((file) => {
|
|
69
|
-
console.log(chalk.red(file));
|
|
70
|
-
});
|
|
71
|
-
}
|
|
50
|
+
await scan(chalk, path, options);
|
|
72
51
|
});
|
|
52
|
+
|
|
53
|
+
program
|
|
54
|
+
.command('qlean-image')
|
|
55
|
+
.description("Scan the project for unused images")
|
|
56
|
+
.argument("<path>", "The path to the directory to scan for unused images")
|
|
57
|
+
.option("-e, --exclude-dir <dir...>", "Exclude directories from the scan")
|
|
58
|
+
.option("-f, --exclude-file <file...>", "Exclude files from the scan")
|
|
59
|
+
.option(
|
|
60
|
+
"-F, --exclude-file-print <files...>",
|
|
61
|
+
"Scan but don't print the excluded files"
|
|
62
|
+
)
|
|
63
|
+
.option("-t, --table", "Print the results in a table")
|
|
64
|
+
.action(async (path, options) => {
|
|
65
|
+
|
|
66
|
+
});
|
|
73
67
|
program.parse(process.argv);
|
|
74
68
|
})();
|
package/command.js
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
const fg = require("fast-glob");
|
|
2
2
|
const fs = require("fs");
|
|
3
|
-
const Table = require(
|
|
3
|
+
const Table = require("cli-table3");
|
|
4
4
|
const parser = require("@babel/parser");
|
|
5
5
|
const traverse = require("@babel/traverse").default;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const { createResolver } = require("./utils/resolver");
|
|
8
|
+
const { loadCache, getFileHash, needsRebuild, saveCache } = require("./utils/cache");
|
|
9
9
|
|
|
10
10
|
async function getFiles(directory = "src", options, chalk) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
11
|
+
const contentPaths = [`${directory}/**/*.{tsx,ts,js,jsx}`];
|
|
12
|
+
if (options.excludeDir && options.excludeDir.length > 0) {
|
|
13
|
+
options.excludeDir.forEach((dir) => {
|
|
14
|
+
contentPaths.push(`!${dir}/**`);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
if (options.excludeFile && options.excludeFile.length > 0) {
|
|
18
|
+
options.excludeFile.forEach((file) => {
|
|
19
|
+
contentPaths.push(`!${directory}/**/${file}`);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const files = await fg(contentPaths);
|
|
23
24
|
const imports = [];
|
|
24
25
|
for (const file of files) {
|
|
25
26
|
const code = fs.readFileSync(file, "utf8");
|
|
@@ -30,13 +31,12 @@ async function getFiles(directory = "src", options, chalk) {
|
|
|
30
31
|
traverse(ast, {
|
|
31
32
|
ImportDeclaration: ({ node }) => {
|
|
32
33
|
imports.push({
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
from: node.source.value,
|
|
35
|
+
file: file,
|
|
36
|
+
line: node.loc.start.line,
|
|
37
|
+
column: node.loc.start.column,
|
|
37
38
|
});
|
|
38
39
|
},
|
|
39
|
-
|
|
40
40
|
});
|
|
41
41
|
// ExportNamedDeclaration: ({ node }) => {
|
|
42
42
|
// if (node.declaration.declarations && node.declaration.declarations[0].id && node.declaration.declarations[0].id.name) {
|
|
@@ -51,123 +51,166 @@ async function getFiles(directory = "src", options, chalk) {
|
|
|
51
51
|
// },
|
|
52
52
|
// });
|
|
53
53
|
}
|
|
54
|
-
|
|
54
|
+
|
|
55
55
|
if (options.table) {
|
|
56
56
|
const tableImports = new Table({
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
head: ["File", "Line", "Column", "Import"],
|
|
58
|
+
colWidths: [20, 10, 10, 20],
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const tableFiles = new Table({
|
|
62
|
+
head: ["File"],
|
|
63
|
+
colWidths: [20],
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (options.listFiles) {
|
|
67
|
+
files.forEach((file) => {
|
|
68
|
+
tableFiles.push([file]);
|
|
59
69
|
});
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
70
|
+
}
|
|
71
|
+
if (options.listImports) {
|
|
72
|
+
imports.forEach((importStatement) => {
|
|
73
|
+
tableImports.push([
|
|
74
|
+
importStatement.file,
|
|
75
|
+
importStatement.line,
|
|
76
|
+
importStatement.column,
|
|
77
|
+
importStatement.from,
|
|
78
|
+
]);
|
|
64
79
|
});
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
tableFiles.push([file]);
|
|
69
|
-
})
|
|
70
|
-
}
|
|
71
|
-
if (options.listImports) {
|
|
72
|
-
imports.forEach((importStatement) => {
|
|
73
|
-
tableImports.push([importStatement.file, importStatement.line, importStatement.column, importStatement.from]);
|
|
74
|
-
})
|
|
75
|
-
}
|
|
76
|
-
return { tableImports, tableFiles };
|
|
77
|
-
}else{
|
|
80
|
+
}
|
|
81
|
+
return { tableImports, tableFiles };
|
|
82
|
+
} else {
|
|
78
83
|
if (options.listFiles) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
84
|
+
console.log(chalk.green("***************** Files *****************"));
|
|
85
|
+
files.forEach((file) => {
|
|
86
|
+
console.log(chalk.green(file));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (options.listImports) {
|
|
90
|
+
console.log(chalk.yellow("***************** Imports *****************"));
|
|
91
|
+
imports.forEach((importStatement) => {
|
|
92
|
+
console.log(
|
|
93
|
+
chalk.yellow(
|
|
94
|
+
`${importStatement.file}:${importStatement.line}:${importStatement.column} ${importStatement.from}`
|
|
95
|
+
)
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return { tableImports: imports, tableFiles: files };
|
|
90
100
|
}
|
|
91
101
|
}
|
|
92
102
|
|
|
93
|
-
async function unUsedFiles(directory = "src", options) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
103
|
+
async function unUsedFiles(chalk, directory = "src", options) {
|
|
104
|
+
console.time('unUsedFiles');
|
|
105
|
+
const resolver = createResolver(directory);
|
|
106
|
+
const cache = loadCache(process.cwd());
|
|
107
|
+
const contentPaths = [`${directory}/**/*.{tsx,ts,js,jsx}`];
|
|
108
|
+
if (options.excludeDir && options.excludeDir.length > 0) {
|
|
109
|
+
options.excludeDir.forEach((dir) => {
|
|
110
|
+
contentPaths.push(`!${dir}/**`);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
if (options.excludeFile && options.excludeFile.length > 0) {
|
|
114
|
+
options.excludeFile.forEach((file) => {
|
|
115
|
+
contentPaths.push(`!${directory}/**/${file}`);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const files = await fg(contentPaths);
|
|
120
|
+
const imports = [];
|
|
121
|
+
const unusedFiles = [];
|
|
122
|
+
|
|
123
|
+
// debug log
|
|
124
|
+
// let debugCount = 0;
|
|
125
|
+
// console.log('total files---', files.length);
|
|
126
|
+
for (const file of files) {
|
|
127
|
+
const code = fs.readFileSync(file, "utf8");
|
|
128
|
+
if(needsRebuild(file, code, cache)) {
|
|
111
129
|
const ast = parser.parse(code, {
|
|
112
130
|
sourceType: "module",
|
|
113
|
-
plugins: [
|
|
114
|
-
"jsx",
|
|
115
|
-
"typescript",
|
|
116
|
-
],
|
|
131
|
+
plugins: ["jsx", "typescript"],
|
|
117
132
|
});
|
|
118
|
-
|
|
133
|
+
cache[file] = {
|
|
134
|
+
hash: getFileHash(code),
|
|
135
|
+
imports: [],
|
|
136
|
+
isImported: false,
|
|
137
|
+
lastModified: fs.statSync(file).mtime.getTime(),
|
|
138
|
+
};
|
|
119
139
|
traverse(ast, {
|
|
120
140
|
ImportDeclaration: ({ node }) => {
|
|
121
141
|
imports.push({
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
142
|
+
from: node.source.value,
|
|
143
|
+
file: file,
|
|
144
|
+
line: node.loc.start.line,
|
|
145
|
+
column: node.loc.start.column,
|
|
146
|
+
});
|
|
147
|
+
cache[file].imports.push({
|
|
148
|
+
from: node.source.value,
|
|
149
|
+
file: file,
|
|
150
|
+
line: node.loc.start.line,
|
|
151
|
+
column: node.loc.start.column,
|
|
152
|
+
lastModified: fs.statSync(file).mtime.getTime(),
|
|
126
153
|
});
|
|
127
154
|
},
|
|
128
155
|
});
|
|
156
|
+
}else {
|
|
157
|
+
// debugCount++;
|
|
158
|
+
// console.log('cache hit', debugCount);
|
|
129
159
|
}
|
|
160
|
+
}
|
|
130
161
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
162
|
+
// console.log('imports', imports);
|
|
163
|
+
// console.log('files', files);
|
|
164
|
+
// debugCount = 0;
|
|
165
|
+
// console.log('total files', files.length);
|
|
166
|
+
for (const file of files) {
|
|
167
|
+
const code = fs.readFileSync(file, "utf8");
|
|
168
|
+
if(!cache[file].isImported || needsRebuild(file, code, cache)) {
|
|
169
|
+
let i = 0;
|
|
170
|
+
// console.log('Checking', file);
|
|
171
|
+
let isFound = false;
|
|
172
|
+
while (!isFound && i < imports.length) {
|
|
173
|
+
const importFilePath = await resolver(
|
|
174
|
+
chalk,
|
|
175
|
+
imports[i].file,
|
|
176
|
+
imports[i].from
|
|
177
|
+
);
|
|
178
|
+
// console.log(chalk.blue('importFilePath'), importFilePath);
|
|
179
|
+
if (compareFiles(path.resolve(file), importFilePath)) {
|
|
180
|
+
isFound = true;
|
|
181
|
+
cache[file].isImported = true;
|
|
182
|
+
break;
|
|
183
|
+
} else if (i === imports.length - 1) {
|
|
184
|
+
if (options.excludeFilePrint && options.excludeFilePrint.length > 0) {
|
|
185
|
+
if (!isExcludedFile(file, options.excludeFilePrint)) {
|
|
186
|
+
unusedFiles.push(file);
|
|
151
187
|
}
|
|
152
|
-
|
|
188
|
+
} else {
|
|
189
|
+
unusedFiles.push(file);
|
|
190
|
+
}
|
|
191
|
+
break;
|
|
153
192
|
}
|
|
193
|
+
i++;
|
|
194
|
+
}
|
|
195
|
+
}else {
|
|
196
|
+
// debugCount++;
|
|
197
|
+
// console.log('debug hit', debugCount);
|
|
154
198
|
}
|
|
155
|
-
|
|
199
|
+
}
|
|
200
|
+
console.timeEnd('unUsedFiles');
|
|
201
|
+
saveCache(process.cwd(), cache);
|
|
202
|
+
return unusedFiles;
|
|
156
203
|
}
|
|
157
204
|
|
|
158
205
|
function isExcludedFile(file, excludeFiles) {
|
|
159
|
-
|
|
206
|
+
return excludeFiles.some((exclude) => file.includes(exclude));
|
|
160
207
|
}
|
|
161
208
|
|
|
162
|
-
function compareFiles(filePath, importPath) {
|
|
163
|
-
|
|
164
|
-
const prefixImportPath = importNoExt.replace(/^(?:\.{1,2}\/|[@~]+\/)+/, "");
|
|
165
|
-
// console.log('importNoExt', importNoExt, ' prefixImportPath', prefixImportPath, ' filePath', filePath, ' ', filePath.includes(prefixImportPath));
|
|
166
|
-
|
|
167
|
-
return filePath.includes(prefixImportPath);
|
|
209
|
+
function compareFiles(filePath, importPath) {
|
|
210
|
+
return filePath === importPath;
|
|
168
211
|
}
|
|
169
212
|
|
|
170
213
|
module.exports = {
|
|
171
214
|
getFiles,
|
|
172
215
|
unUsedFiles,
|
|
173
|
-
};
|
|
216
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const { getFiles, unUsedFiles } = require("../command");
|
|
2
|
+
const Table = require('cli-table3');
|
|
3
|
+
const askDeleteFiles = require("../utils/utils");
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
async function list(chalk, path, options) {
|
|
7
|
+
const { tableImports, tableFiles } = await getFiles(path, options, chalk);
|
|
8
|
+
if(options.table){
|
|
9
|
+
console.log(chalk.yellow('***************** Imported Files *****************'));
|
|
10
|
+
console.log(tableImports.toString());
|
|
11
|
+
console.log(chalk.green('***************** List Files *****************'));
|
|
12
|
+
console.log(tableFiles.toString());
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function scan(chalk, path, options) {
|
|
17
|
+
const unusedFiles = await unUsedFiles(chalk,path, options);
|
|
18
|
+
// console.clear()
|
|
19
|
+
|
|
20
|
+
if (options.dryRun) {
|
|
21
|
+
console.log(chalk.cyan('\n[DRY RUN MODE] No files will be deleted\n'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if(options.table){
|
|
25
|
+
const table = new Table({
|
|
26
|
+
head: options.dryRun ? ['Unused Files (Would Delete)'] : ['Unused Files'],
|
|
27
|
+
colWidths: [50]
|
|
28
|
+
})
|
|
29
|
+
unusedFiles.forEach(file => {
|
|
30
|
+
table.push([file])
|
|
31
|
+
})
|
|
32
|
+
console.log(table.toString())
|
|
33
|
+
|
|
34
|
+
}else {
|
|
35
|
+
unusedFiles.forEach((file) => {
|
|
36
|
+
console.log(chalk.red(file));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (options.dryRun && unusedFiles.length > 0) {
|
|
41
|
+
console.log(chalk.cyan(`\n[DRY RUN] Would delete ${unusedFiles.length} file(s)`));
|
|
42
|
+
console.log(chalk.cyan('Run without --dry-run to actually delete files\n'));
|
|
43
|
+
} else if (!options.dryRun && unusedFiles.length > 0) {
|
|
44
|
+
askDeleteFiles(unusedFiles);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
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
|
+
module.exports = {
|
|
58
|
+
list,
|
|
59
|
+
scan
|
|
60
|
+
};
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qleaner",
|
|
3
3
|
"packageManager": "yarn@4.6.0",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.12",
|
|
5
5
|
"main": "command.js",
|
|
6
6
|
"bin": "./bin/cli.js",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"start": "node ./bin/cli.js"
|
|
8
|
+
"start": "node ./bin/cli.js",
|
|
9
|
+
"resolve": "node ./utils/resolver.js"
|
|
9
10
|
},
|
|
10
11
|
"devDependencies": {
|
|
11
12
|
"@types/node": "^20.10.5",
|
|
@@ -17,6 +18,8 @@
|
|
|
17
18
|
"chalk": "^5.6.2",
|
|
18
19
|
"cli-table3": "^0.6.5",
|
|
19
20
|
"commander": "^14.0.2",
|
|
20
|
-
"
|
|
21
|
+
"enhanced-resolve": "^5.18.3",
|
|
22
|
+
"fast-glob": "^3.3.3",
|
|
23
|
+
"prompts": "^2.4.2"
|
|
21
24
|
}
|
|
22
25
|
}
|
|
@@ -0,0 +1,180 @@
|
|
|
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
|
+
}
|
package/utils/cache.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
function loadCache(rootPath) {
|
|
7
|
+
const file = path.join(rootPath, "unused-check-cache.json");
|
|
8
|
+
if(!fs.existsSync(file)) return {};
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
12
|
+
} catch{
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getFileHash(content) {
|
|
18
|
+
return crypto.createHash('md5').update(content).digest('hex');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
function needsRebuild(file, content, cache) {
|
|
23
|
+
const hash = getFileHash(content);
|
|
24
|
+
return !cache[file] || cache[file].hash !== hash
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
function saveCache(rootPath, cache) {
|
|
29
|
+
const file = path.join(rootPath, "unused-check-cache.json");
|
|
30
|
+
fs.writeFileSync(file, JSON.stringify(cache, null, 2))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
loadCache,
|
|
35
|
+
getFileHash,
|
|
36
|
+
needsRebuild,
|
|
37
|
+
saveCache
|
|
38
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const {create} = require('enhanced-resolve');
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
function createResolver(directory) {
|
|
6
|
+
const resolver = create({
|
|
7
|
+
extensions: [".js", ".jsx", ".ts", ".tsx"],
|
|
8
|
+
alias: {
|
|
9
|
+
"@": path.resolve(directory),
|
|
10
|
+
"~": path.resolve(directory)
|
|
11
|
+
},
|
|
12
|
+
mainFiles: ["index"],
|
|
13
|
+
// Where resolution begins
|
|
14
|
+
modules: [
|
|
15
|
+
path.resolve(directory)
|
|
16
|
+
]
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return function resolveImport(chalk,sourceFile, importPath) {
|
|
20
|
+
// console.log(chalk.yellow('sourceFile--resolver'), sourceFile, chalk.yellow('importPath--resolver'), importPath);
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
resolver(path.dirname(sourceFile), importPath, (err, result) => {
|
|
23
|
+
if (err) return resolve(null);
|
|
24
|
+
resolve(path.resolve(result));
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
createResolver
|
|
32
|
+
};
|
package/utils/utils.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const prompts = require('prompts');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
async function askDeleteFiles(files) {
|
|
6
|
+
const response = await prompts({
|
|
7
|
+
type: 'multiselect',
|
|
8
|
+
name: 'toDelete',
|
|
9
|
+
message: 'Select unused files to delete or move to .trash directory',
|
|
10
|
+
choices: files.map(file => ({
|
|
11
|
+
title: file,
|
|
12
|
+
value: file,
|
|
13
|
+
}))
|
|
14
|
+
})
|
|
15
|
+
if(response && response.toDelete && response.toDelete.length > 0) {
|
|
16
|
+
const method = await prompts({
|
|
17
|
+
type: 'select',
|
|
18
|
+
name: 'method',
|
|
19
|
+
message: 'Select a method to delete the files',
|
|
20
|
+
choices: [
|
|
21
|
+
{ title: 'Move to .trash directory', value: 'moveToTrash' },
|
|
22
|
+
{ title: 'Delete files', value: 'deleteFiles' },
|
|
23
|
+
]
|
|
24
|
+
})
|
|
25
|
+
if(method.method === 'moveToTrash') {
|
|
26
|
+
return await moveToTrash(response.toDelete);
|
|
27
|
+
} else if(method.method === 'deleteFiles') {
|
|
28
|
+
return await deleteFiles(response.toDelete);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function moveToTrash(files) {
|
|
34
|
+
const trashDir = path.join(process.cwd(), '.trash');
|
|
35
|
+
|
|
36
|
+
if(!fs.existsSync(trashDir)) {
|
|
37
|
+
fs.mkdirSync(trashDir);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for(const file of files) {
|
|
41
|
+
const fileName = path.basename(file);
|
|
42
|
+
const destination = path.join(trashDir, fileName);
|
|
43
|
+
fs.renameSync(file, destination);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log(`Moved ${files.length} files to .trash directory`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function deleteFiles(files) {
|
|
50
|
+
for(const file of files) {
|
|
51
|
+
fs.unlinkSync(file);
|
|
52
|
+
}
|
|
53
|
+
console.log(`Deleted ${files.length} files`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = askDeleteFiles;
|