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/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
- // Builds an array of glob patterns for file discovery, including exclusion patterns
43
- function buildContentPaths(directory, options) {
44
- const contentPaths = [`${directory}/**/*.{tsx,ts,js,jsx}`];
45
- if (options.excludeDir && options.excludeDir.length > 0) {
46
- options.excludeDir.forEach((dir) => {
47
- contentPaths.push(`!${dir}/**`);
48
- });
49
- }
50
- if (options.excludeFile && options.excludeFile.length > 0) {
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
- const scanBar = createStepBar(
66
- `1/${files.length}`,
67
- files.length,
68
- "Scanning files",
69
- chalk,
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
- let packingBar = null;
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
- if (arg?.type === "StringLiteral") {
111
- imports.push({
112
- file: filePath,
113
- source: arg.value,
114
- type: "dynamic",
115
- });
116
- } else {
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 (imports.length > 0) {
131
- packingBar = createStepBar(
132
- `1/${imports.length}`,
133
- imports.length,
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
- if (packingBar) {
176
- packingBar.stop();
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
- let compareBar = null;
180
- if (oldPaths.size > 0) {
181
- compareBar = createStepBar(
182
- `1/${oldPaths.size}`,
183
- oldPaths.size,
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
- // count the number of removed files
189
- let removedFilesCount = 0;
190
- // compare old paths with new paths
191
- // if some old paths are found missing in new paths, remove them from the graph
192
- for (const [filePath, oldFiles] of oldPaths) {
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
- if (compareBar) {
214
- compareBar.stop();
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
- // return the number of imports or the number of removed files
217
- return imports.length || removedFilesCount;
126
+ return trackExportedComponents(reExportedBy, exportedComponents, parentGraph);
218
127
  }
219
128
 
220
- // Checks if a single file is imported/used by comparing it against all import statements
221
- // If not found in any imports and not excluded, adds it to the unused files set
222
- async function checkFileUsage(file, parentGraph, options) {
223
- if (
224
- parentGraph.graph.has(file) &&
225
- parentGraph.graph.get(file).importedBy.size === 0
226
- ) {
227
- if (options.excludeFilePrint && options.excludeFilePrint.length > 0) {
228
- if (!isExcludedFile(file, options.excludeFilePrint)) {
229
- parentGraph.unusedFiles.add(parentGraph.graph.get(file));
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
  };