qleaner 1.2.0 → 1.3.0
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 +177 -67
- package/command.js +137 -202
- package/controllers/image.js +146 -508
- package/controllers/initialize.js +4 -2
- package/controllers/list.js +21 -7
- package/controllers/summary.js +176 -143
- package/package.json +2 -4
- package/qleaner.config.json +26 -2
- package/test.js +2 -0
- package/utils/astParser.js +224 -0
- package/utils/cache.js +15 -0
- package/utils/constants.js +29 -3
- package/utils/cssImages.js +130 -51
- package/utils/fileProcessing.js +184 -0
- package/utils/graphComparison.js +144 -0
- package/utils/graphOperations.js +141 -0
- package/utils/graphUtils.js +32 -0
- package/utils/imageAstParser.js +220 -0
- package/utils/imageDisplay.js +113 -0
- package/utils/imageExtractors.js +82 -0
- package/utils/imageGraphUtils.js +96 -0
- package/utils/imagePathBuilder.js +50 -0
- package/utils/pathBuilder.js +33 -0
- package/utils/resolver.js +58 -7
- package/utils/summary.js +32 -7
- package/utils/utils.js +139 -89
package/command.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
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
2
|
const path = require("path");
|
|
6
3
|
const { createResolver } = require("./utils/resolver");
|
|
7
|
-
const {
|
|
8
|
-
loadCache,
|
|
9
|
-
getFileHash,
|
|
10
|
-
needsRebuild,
|
|
11
|
-
saveCache,
|
|
12
|
-
} = require("./utils/cache");
|
|
4
|
+
const { loadCache, saveCache } = require("./utils/cache");
|
|
13
5
|
const { isExcludedFile, createStepBar } = require("./utils/utils");
|
|
6
|
+
const { buildContentPaths } = require("./utils/pathBuilder");
|
|
7
|
+
const { hydrateGraph } = require("./utils/graphUtils");
|
|
8
|
+
const {
|
|
9
|
+
scanFilesForImportsAndExports,
|
|
10
|
+
processImports,
|
|
11
|
+
processExports,
|
|
12
|
+
} = require("./utils/fileProcessing");
|
|
13
|
+
const {
|
|
14
|
+
compareAndRemoveOldImports,
|
|
15
|
+
compareAndRemoveOldReExports,
|
|
16
|
+
compareAndRemoveOldExports,
|
|
17
|
+
} = require("./utils/graphComparison");
|
|
14
18
|
|
|
15
19
|
// Initializes the cache by clearing it (if requested) and loading it from disk
|
|
16
20
|
function initializeCache(spinner, options, code = true) {
|
|
@@ -39,199 +43,142 @@ function initializeCache(spinner, options, code = true) {
|
|
|
39
43
|
};
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
options.excludeFile.forEach((file) => {
|
|
52
|
-
contentPaths.push(`!${directory}/**/${file}`);
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
if (options.excludeExtensions && options.excludeExtensions.length > 0) {
|
|
56
|
-
options.excludeExtensions.forEach((extension) => {
|
|
57
|
-
contentPaths.push(`!${directory}/**/*.${extension}`);
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
return contentPaths;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Extracts import statements from files by parsing AST, only processes files that need rebuilding
|
|
46
|
+
/**
|
|
47
|
+
* Extracts import statements from files by parsing AST, only processes files that need rebuilding
|
|
48
|
+
* Orchestrates the scanning, processing, and comparison of imports/exports
|
|
49
|
+
* @param {Array<string>} files - Array of file paths to process
|
|
50
|
+
* @param {Map} graph - The dependency graph
|
|
51
|
+
* @param {Function} resolver - Resolver function to resolve import/export paths
|
|
52
|
+
* @param {Object} chalk - Chalk instance for progress bars
|
|
53
|
+
* @returns {number} Number indicating changes (imports count or removed items count)
|
|
54
|
+
*/
|
|
64
55
|
async function extractImportsFromFiles(files, graph, resolver, chalk) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
files
|
|
68
|
-
|
|
69
|
-
|
|
56
|
+
// Step 1: Scan files and extract imports/exports from AST
|
|
57
|
+
const { imports, exports, oldPaths, oldExports, oldReExported } =
|
|
58
|
+
await scanFilesForImportsAndExports(files, graph, chalk);
|
|
59
|
+
// Step 2: Process imports and add them to the graph
|
|
60
|
+
await processImports(imports, graph, resolver, chalk);
|
|
61
|
+
// console.dir(imports, { depth: null });
|
|
62
|
+
// Step 3: Process exports and add them to the graph
|
|
63
|
+
await processExports(exports, graph, resolver, chalk);
|
|
64
|
+
// console.dir(exports, { depth: null });
|
|
65
|
+
|
|
66
|
+
// Step 4: Compare old state with new state and remove unused items
|
|
67
|
+
// These operations are independent and can run in parallel
|
|
68
|
+
const [removedFilesCount, removedReExportedCount, removedExportsCount] =
|
|
69
|
+
await Promise.all([
|
|
70
|
+
compareAndRemoveOldImports(oldPaths, graph, chalk),
|
|
71
|
+
compareAndRemoveOldReExports(oldReExported, graph, chalk),
|
|
72
|
+
compareAndRemoveOldExports(oldExports, graph, chalk),
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
// Return the number of imports or the number of removed files
|
|
76
|
+
return (
|
|
77
|
+
imports.length ||
|
|
78
|
+
removedFilesCount ||
|
|
79
|
+
removedReExportedCount ||
|
|
80
|
+
removedExportsCount
|
|
70
81
|
);
|
|
71
|
-
|
|
72
|
-
const imports = [];
|
|
73
|
-
const oldPaths = new Map();
|
|
74
|
-
for (const file of files) {
|
|
75
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
76
|
-
scanBar.increment();
|
|
77
|
-
const filePath = path.resolve(file);
|
|
78
|
-
const code = fs.readFileSync(filePath, "utf8");
|
|
79
|
-
const isNeedsRebuild = needsRebuild(filePath, code, graph);
|
|
80
|
-
if (isNeedsRebuild) {
|
|
81
|
-
const ast = parser.parse(code, {
|
|
82
|
-
sourceType: "module",
|
|
83
|
-
plugins: ["jsx", "typescript", "decorators-legacy"],
|
|
84
|
-
});
|
|
85
|
-
// check if file is already in graph
|
|
86
|
-
if (graph.has(filePath)) {
|
|
87
|
-
const oldFiles = new Set(graph.get(filePath).imports);
|
|
88
|
-
oldPaths.set(filePath, new Set(oldFiles));
|
|
89
|
-
}
|
|
90
|
-
// create file graph node if it doesn't exist
|
|
91
|
-
graph.set(filePath, {
|
|
92
|
-
file: filePath,
|
|
93
|
-
hash: getFileHash(code),
|
|
94
|
-
imports: new Set(),
|
|
95
|
-
importedBy: new Set(),
|
|
96
|
-
lastModified: fs.statSync(filePath).mtime.getTime(),
|
|
97
|
-
size: fs.statSync(filePath).size,
|
|
98
|
-
});
|
|
99
|
-
traverse(ast, {
|
|
100
|
-
ImportDeclaration: ({ node }) => {
|
|
101
|
-
imports.push({
|
|
102
|
-
file: filePath,
|
|
103
|
-
source: node.source.value,
|
|
104
|
-
});
|
|
105
|
-
},
|
|
106
|
-
CallExpression({ node }) {
|
|
107
|
-
if (node.callee.type === "Import") {
|
|
108
|
-
const arg = node.arguments[0];
|
|
82
|
+
}
|
|
109
83
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
imports.push({
|
|
118
|
-
file: filePath,
|
|
119
|
-
source: null,
|
|
120
|
-
type: "dynamic-variable",
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
});
|
|
126
|
-
}
|
|
84
|
+
// Checks if a single file is imported/used by comparing it against all import statements
|
|
85
|
+
// If not found in any imports and not excluded, adds it to the unused files set
|
|
86
|
+
async function checkFileUsage(file, parentGraph, options) {
|
|
87
|
+
// Cache the file node to avoid repeated graph lookups
|
|
88
|
+
const fileNode = parentGraph.graph.get(file);
|
|
89
|
+
if (!fileNode || fileNode.importedBy.size > 0) {
|
|
90
|
+
return;
|
|
127
91
|
}
|
|
128
|
-
scanBar.stop();
|
|
129
92
|
|
|
130
|
-
if
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
"Packing imports",
|
|
135
|
-
chalk,
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
for (const info of imports) {
|
|
139
|
-
if (packingBar) {
|
|
140
|
-
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
141
|
-
packingBar.increment();
|
|
142
|
-
}
|
|
143
|
-
const { importPath, isMightBeModule } = await resolver(
|
|
144
|
-
info.file,
|
|
145
|
-
info.source,
|
|
146
|
-
);
|
|
147
|
-
if (!isMightBeModule) {
|
|
148
|
-
graph.get(info.file).imports.add(importPath);
|
|
149
|
-
if (!graph.has(importPath)) {
|
|
150
|
-
graph.set(importPath, {
|
|
151
|
-
file: importPath,
|
|
152
|
-
size: fs.statSync(importPath).size,
|
|
153
|
-
hash: getFileHash(fs.readFileSync(importPath, "utf8")),
|
|
154
|
-
imports: new Set(),
|
|
155
|
-
importedBy: new Set(),
|
|
156
|
-
lastModified: fs.statSync(importPath).mtime.getTime(),
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
graph.get(importPath).importedBy.add(info.file);
|
|
160
|
-
} else {
|
|
161
|
-
graph.get(info.file).imports.add(importPath);
|
|
162
|
-
if (!graph.has(importPath)) {
|
|
163
|
-
graph.set(importPath, {
|
|
164
|
-
file: importPath,
|
|
165
|
-
size: 0,
|
|
166
|
-
hash: null,
|
|
167
|
-
imports: new Set(),
|
|
168
|
-
importedBy: new Set(),
|
|
169
|
-
lastModified: null,
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
graph.get(importPath).importedBy.add(info.file);
|
|
93
|
+
// Check if file should be excluded
|
|
94
|
+
if (options.excludeFilePrint && options.excludeFilePrint.length > 0) {
|
|
95
|
+
if (isExcludedFile(file, options.excludeFilePrint)) {
|
|
96
|
+
return;
|
|
173
97
|
}
|
|
174
98
|
}
|
|
175
|
-
|
|
176
|
-
|
|
99
|
+
|
|
100
|
+
// Check if the file has a component name imported from the shared file
|
|
101
|
+
// It should not be considered unused if components are used via re-exports
|
|
102
|
+
const isUsed = checkImportedComponentsUsage(file, parentGraph);
|
|
103
|
+
if (!isUsed) {
|
|
104
|
+
parentGraph.unusedFiles.add(fileNode);
|
|
177
105
|
}
|
|
106
|
+
}
|
|
178
107
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
"Comparing old paths with new paths",
|
|
185
|
-
chalk,
|
|
186
|
-
);
|
|
108
|
+
function checkImportedComponentsUsage(file, parentGraph) {
|
|
109
|
+
// Cache the file node to avoid repeated graph lookups
|
|
110
|
+
const fileNode = parentGraph.graph.get(file);
|
|
111
|
+
if (!fileNode) {
|
|
112
|
+
return false;
|
|
187
113
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (compareBar) {
|
|
194
|
-
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
195
|
-
compareBar.increment();
|
|
196
|
-
}
|
|
197
|
-
if (graph.has(filePath)) {
|
|
198
|
-
// Find files that were in oldFiles but are no longer in current imports
|
|
199
|
-
const removedFiles = new Set(
|
|
200
|
-
[...oldFiles].filter((x) => !graph.get(filePath).imports.has(x)),
|
|
201
|
-
);
|
|
202
|
-
if (removedFiles.size > 0) {
|
|
203
|
-
// remove the removed files from the graph
|
|
204
|
-
removedFiles.forEach((file) => {
|
|
205
|
-
if (graph.has(file)) {
|
|
206
|
-
graph.get(file).importedBy.delete(filePath);
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
removedFilesCount++;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
114
|
+
|
|
115
|
+
// Early exit if file is not re-exported
|
|
116
|
+
const reExportedBy = fileNode.reExportedBy;
|
|
117
|
+
if (reExportedBy.size === 0) {
|
|
118
|
+
return false;
|
|
212
119
|
}
|
|
213
|
-
|
|
214
|
-
|
|
120
|
+
|
|
121
|
+
// Cache exported components and check once upfront
|
|
122
|
+
const exportedComponents = fileNode.exports;
|
|
123
|
+
if (exportedComponents.size === 0) {
|
|
124
|
+
return false;
|
|
215
125
|
}
|
|
216
|
-
|
|
217
|
-
return imports.length || removedFilesCount;
|
|
126
|
+
return trackExportedComponents(reExportedBy, exportedComponents, parentGraph);
|
|
218
127
|
}
|
|
219
128
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
129
|
+
function trackExportedComponents(reExportedBy, exportedComponents, parentGraph) {
|
|
130
|
+
let isUsed = false;
|
|
131
|
+
const notUsed = false
|
|
132
|
+
// Loop through the re-exported files, check if there is a file that imports any of the components or *
|
|
133
|
+
for (const reExportingFile of reExportedBy) {
|
|
134
|
+
// Cache the re-exporting file node
|
|
135
|
+
const reExportingNode = parentGraph.graph.get(reExportingFile);
|
|
136
|
+
if (!reExportingNode) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const importedBy = reExportingNode.importedBy; // A.ts -> B.ts
|
|
141
|
+
// if (importedBy.size === 0) {
|
|
142
|
+
// continue;
|
|
143
|
+
// }
|
|
144
|
+
|
|
145
|
+
// check reExportedBy if available
|
|
146
|
+
const reExportedByFiles = reExportingNode.reExportedBy;
|
|
147
|
+
if (reExportedByFiles.size > 0) {
|
|
148
|
+
isUsed = trackExportedComponents(reExportedByFiles, exportedComponents, parentGraph);
|
|
149
|
+
}
|
|
150
|
+
// Loop through the importing files, check if they import any of the components or *
|
|
151
|
+
for (const importingFile of importedBy) {
|
|
152
|
+
// Cache the importing file node
|
|
153
|
+
const importingNode = parentGraph.graph.get(importingFile);
|
|
154
|
+
if (!importingNode) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const imported = importingNode.imported; // B.ts -> A.ts -> B.ts
|
|
159
|
+
if (!imported || !imported.has(reExportingFile)) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const components = imported.get(reExportingFile); // B.ts -> A.ts -> B.ts [['ComponentD', 'D2'], ['ComponentC', 'ComponentC']]
|
|
164
|
+
if (!components || components.size === 0) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Check if any imported component matches exported components or is a wildcard
|
|
169
|
+
for (const component of components) {
|
|
170
|
+
if (
|
|
171
|
+
exportedComponents.has(component[0]) ||
|
|
172
|
+
exportedComponents.has(component[1]) ||
|
|
173
|
+
component[0] === "*" ||
|
|
174
|
+
component[1] === "*"
|
|
175
|
+
) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
230
179
|
}
|
|
231
|
-
} else {
|
|
232
|
-
parentGraph.unusedFiles.add(parentGraph.graph.get(file));
|
|
233
180
|
}
|
|
234
|
-
|
|
181
|
+
return notUsed || isUsed;
|
|
235
182
|
}
|
|
236
183
|
|
|
237
184
|
// Checks all files to determine which ones are unused by comparing against import statements
|
|
@@ -241,7 +188,7 @@ async function checkUnusedFiles(files, parentGraph, options, chalk) {
|
|
|
241
188
|
`1/${files.length}`,
|
|
242
189
|
files.length,
|
|
243
190
|
"Checking unused files",
|
|
244
|
-
chalk
|
|
191
|
+
chalk
|
|
245
192
|
);
|
|
246
193
|
for (const file of files) {
|
|
247
194
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
@@ -258,24 +205,13 @@ function finalizeCacheAndReturn(parentGraph, spinner, imageParentGraph) {
|
|
|
258
205
|
return parentGraph.unusedFiles;
|
|
259
206
|
}
|
|
260
207
|
|
|
261
|
-
function hydrateGraph(graphData) {
|
|
262
|
-
const graph = new Map();
|
|
263
|
-
for (const [file, data] of Object.entries(graphData)) {
|
|
264
|
-
graph.set(file, {
|
|
265
|
-
...data,
|
|
266
|
-
imports: new Set(data.imports),
|
|
267
|
-
importedBy: new Set(data.importedBy),
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
return graph;
|
|
271
|
-
}
|
|
272
208
|
// Main function that orchestrates the unused files detection process
|
|
273
209
|
// Step 1: Discovers files, Step 2: Extracts imports, Step 3: Checks which files are unused
|
|
274
|
-
async function unUsedFiles(ora, chalk, directory = "src", options) {
|
|
210
|
+
async function unUsedFiles(ora, chalk, directory = "src", pathConfig = null, options) {
|
|
275
211
|
// Start spinner
|
|
276
212
|
const spinner = ora("Start Qleaner scan...").start();
|
|
277
213
|
|
|
278
|
-
const resolver = createResolver(directory);
|
|
214
|
+
const resolver = createResolver(directory, pathConfig);
|
|
279
215
|
const { parentGraph, imageParentGraph } = initializeCache(spinner, options);
|
|
280
216
|
const contentPaths = buildContentPaths(directory, options);
|
|
281
217
|
|
|
@@ -292,7 +228,7 @@ async function unUsedFiles(ora, chalk, directory = "src", options) {
|
|
|
292
228
|
files,
|
|
293
229
|
parentGraph.graph,
|
|
294
230
|
resolver,
|
|
295
|
-
chalk
|
|
231
|
+
chalk
|
|
296
232
|
);
|
|
297
233
|
|
|
298
234
|
spinner.succeed(`Checked ${files.length} files`);
|
|
@@ -312,5 +248,4 @@ async function unUsedFiles(ora, chalk, directory = "src", options) {
|
|
|
312
248
|
module.exports = {
|
|
313
249
|
unUsedFiles,
|
|
314
250
|
initializeCache,
|
|
315
|
-
hydrateGraph,
|
|
316
251
|
};
|