codesummary 1.0.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/src/scanner.js ADDED
@@ -0,0 +1,330 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import ErrorHandler from './errorHandler.js';
5
+
6
+ /**
7
+ * File Scanner for CodeSummary
8
+ * Handles recursive directory traversal and file filtering
9
+ */
10
+ export class Scanner {
11
+ constructor(config) {
12
+ this.config = config;
13
+ this.allowedExtensions = new Set(config.allowedExtensions.map(ext => ext.toLowerCase()));
14
+ this.excludeDirs = new Set(config.excludeDirs);
15
+ }
16
+
17
+ /**
18
+ * Scan a directory recursively and return files grouped by extension
19
+ * @param {string} rootPath - Root directory to scan
20
+ * @returns {Promise<object>} Object with extensions as keys and file arrays as values
21
+ */
22
+ async scanDirectory(rootPath) {
23
+ try {
24
+ // Validate and sanitize input path
25
+ const sanitizedPath = ErrorHandler.sanitizeInput(rootPath);
26
+ ErrorHandler.validatePath(sanitizedPath, { mustExist: true, preventTraversal: true });
27
+
28
+ const resolvedRoot = path.resolve(sanitizedPath);
29
+ const stats = await fs.stat(resolvedRoot);
30
+
31
+ if (!stats.isDirectory()) {
32
+ throw new Error(`Path is not a directory: ${resolvedRoot}`);
33
+ }
34
+
35
+ console.log(chalk.gray(`Scanning directory: ${resolvedRoot}`));
36
+
37
+ const filesByExtension = {};
38
+ const scannedFiles = new Set(); // Prevent duplicates
39
+
40
+ await this.walkDirectory(resolvedRoot, resolvedRoot, filesByExtension, scannedFiles);
41
+
42
+ // Sort files within each extension group
43
+ Object.keys(filesByExtension).forEach(ext => {
44
+ filesByExtension[ext].sort((a, b) => a.relativePath.localeCompare(b.relativePath));
45
+ });
46
+
47
+ return filesByExtension;
48
+ } catch (error) {
49
+ if (error.code === 'ENOENT') {
50
+ throw new Error(`Directory does not exist: ${rootPath}`);
51
+ } else if (error.code === 'EACCES') {
52
+ throw new Error(`Permission denied accessing directory: ${rootPath}`);
53
+ }
54
+ throw error;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Recursively walk through directory structure
60
+ * @param {string} currentPath - Current directory being processed
61
+ * @param {string} rootPath - Original root path for relative path calculation
62
+ * @param {object} filesByExtension - Accumulator object for results
63
+ * @param {Set} scannedFiles - Set to track processed files and avoid duplicates
64
+ */
65
+ async walkDirectory(currentPath, rootPath, filesByExtension, scannedFiles) {
66
+ try {
67
+ const entries = await fs.readdir(currentPath, { withFileTypes: true });
68
+
69
+ for (const entry of entries) {
70
+ const fullPath = path.join(currentPath, entry.name);
71
+ const relativePath = path.relative(rootPath, fullPath);
72
+
73
+ if (entry.isDirectory()) {
74
+ // Skip excluded directories and hidden directories (unless explicitly allowed)
75
+ if (this.shouldSkipDirectory(entry.name, relativePath)) {
76
+ continue;
77
+ }
78
+
79
+ // Recursively scan subdirectory
80
+ await this.walkDirectory(fullPath, rootPath, filesByExtension, scannedFiles);
81
+ } else if (entry.isFile()) {
82
+ // Process file if it matches criteria
83
+ await this.processFile(fullPath, rootPath, filesByExtension, scannedFiles);
84
+ }
85
+ // Skip symbolic links, special files, etc.
86
+ }
87
+ } catch (error) {
88
+ // Handle different types of file system errors appropriately
89
+ if (error.code === 'EACCES' || error.code === 'EPERM') {
90
+ console.warn(chalk.yellow(`WARNING: Permission denied: ${currentPath}`));
91
+ } else if (error.code === 'ENOENT') {
92
+ console.warn(chalk.yellow(`WARNING: Directory no longer exists: ${currentPath}`));
93
+ } else {
94
+ console.warn(chalk.yellow(`WARNING: Cannot read directory ${currentPath}: ${error.message}`));
95
+ }
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Process a single file and add it to results if it matches criteria
101
+ * @param {string} fullPath - Absolute path to the file
102
+ * @param {string} rootPath - Root path for relative calculation
103
+ * @param {object} filesByExtension - Results accumulator
104
+ * @param {Set} scannedFiles - Set of already processed files
105
+ */
106
+ async processFile(fullPath, rootPath, filesByExtension, scannedFiles) {
107
+ try {
108
+ const relativePath = path.relative(rootPath, fullPath);
109
+
110
+ // Avoid processing the same file twice (in case of symlinks)
111
+ if (scannedFiles.has(fullPath)) {
112
+ return;
113
+ }
114
+ scannedFiles.add(fullPath);
115
+
116
+ const extension = path.extname(relativePath).toLowerCase();
117
+
118
+ // Skip files without extensions or not in allowed list
119
+ if (!extension || !this.allowedExtensions.has(extension)) {
120
+ return;
121
+ }
122
+
123
+ // Skip hidden files (starting with .) unless explicitly needed
124
+ const fileName = path.basename(relativePath);
125
+ if (fileName.startsWith('.') && !this.isAllowedHiddenFile(fileName)) {
126
+ return;
127
+ }
128
+
129
+ // Verify file is readable
130
+ const stats = await fs.stat(fullPath);
131
+ if (!stats.isFile()) {
132
+ return;
133
+ }
134
+
135
+ // Add to results
136
+ if (!filesByExtension[extension]) {
137
+ filesByExtension[extension] = [];
138
+ }
139
+
140
+ filesByExtension[extension].push({
141
+ relativePath: relativePath.replace(/\\/g, '/'), // Normalize path separators
142
+ absolutePath: fullPath,
143
+ size: stats.size,
144
+ modified: stats.mtime
145
+ });
146
+
147
+ } catch (error) {
148
+ // Handle file processing errors with appropriate context
149
+ if (error.code === 'EACCES' || error.code === 'EPERM') {
150
+ console.warn(chalk.yellow(`WARNING: Permission denied: ${relativePath}`));
151
+ } else if (error.code === 'ENOENT') {
152
+ // File might have been deleted during scan
153
+ console.warn(chalk.yellow(`WARNING: File no longer exists: ${relativePath}`));
154
+ } else {
155
+ console.warn(chalk.yellow(`WARNING: Cannot process file ${relativePath}: ${error.message}`));
156
+ }
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Determine if a directory should be skipped
162
+ * @param {string} dirName - Directory name
163
+ * @param {string} relativePath - Relative path from root
164
+ * @returns {boolean} True if directory should be skipped
165
+ */
166
+ shouldSkipDirectory(dirName, relativePath) {
167
+ // Skip directories in exclude list
168
+ if (this.excludeDirs.has(dirName)) {
169
+ return true;
170
+ }
171
+
172
+ // Skip hidden directories (starting with .) unless explicitly allowed
173
+ if (dirName.startsWith('.') && !this.isAllowedHiddenDirectory(dirName)) {
174
+ return true;
175
+ }
176
+
177
+ // Skip common build/cache directories that might not be in exclude list
178
+ const commonSkipDirs = new Set([
179
+ 'tmp', 'temp', 'cache', '.cache', 'logs', '.logs',
180
+ 'bower_components', 'vendor', '.vendor'
181
+ ]);
182
+
183
+ if (commonSkipDirs.has(dirName.toLowerCase())) {
184
+ return true;
185
+ }
186
+
187
+ return false;
188
+ }
189
+
190
+ /**
191
+ * Check if a hidden file should be included
192
+ * @param {string} fileName - File name
193
+ * @returns {boolean} True if file should be included
194
+ */
195
+ isAllowedHiddenFile(fileName) {
196
+ const allowedHiddenFiles = new Set([
197
+ '.gitignore', '.gitattributes', '.editorconfig',
198
+ '.eslintrc.js', '.eslintrc.json', '.prettierrc',
199
+ '.env.example', '.htaccess'
200
+ ]);
201
+
202
+ return allowedHiddenFiles.has(fileName);
203
+ }
204
+
205
+ /**
206
+ * Check if a hidden directory should be included
207
+ * @param {string} dirName - Directory name
208
+ * @returns {boolean} True if directory should be included
209
+ */
210
+ isAllowedHiddenDirectory(dirName) {
211
+ const allowedHiddenDirs = new Set([
212
+ '.github', '.gitlab', '.circleci'
213
+ ]);
214
+
215
+ return allowedHiddenDirs.has(dirName);
216
+ }
217
+
218
+ /**
219
+ * Get file extension descriptions for user display
220
+ * @param {object} filesByExtension - Files grouped by extension
221
+ * @returns {Array} Array of extension info objects
222
+ */
223
+ getExtensionInfo(filesByExtension) {
224
+ const extensionDescriptions = {
225
+ '.js': 'JavaScript',
226
+ '.ts': 'TypeScript',
227
+ '.jsx': 'React JSX',
228
+ '.tsx': 'TypeScript JSX',
229
+ '.json': 'JSON',
230
+ '.xml': 'XML',
231
+ '.html': 'HTML',
232
+ '.css': 'CSS',
233
+ '.scss': 'SCSS',
234
+ '.sass': 'Sass',
235
+ '.md': 'Markdown',
236
+ '.txt': 'Text',
237
+ '.py': 'Python',
238
+ '.java': 'Java',
239
+ '.cs': 'C#',
240
+ '.cpp': 'C++',
241
+ '.c': 'C',
242
+ '.h': 'Header',
243
+ '.yaml': 'YAML',
244
+ '.yml': 'YAML',
245
+ '.sh': 'Shell Script',
246
+ '.bat': 'Batch File',
247
+ '.ps1': 'PowerShell',
248
+ '.php': 'PHP',
249
+ '.rb': 'Ruby',
250
+ '.go': 'Go',
251
+ '.rs': 'Rust',
252
+ '.swift': 'Swift',
253
+ '.kt': 'Kotlin',
254
+ '.scala': 'Scala',
255
+ '.vue': 'Vue.js',
256
+ '.svelte': 'Svelte',
257
+ '.dockerfile': 'Dockerfile',
258
+ '.sql': 'SQL',
259
+ '.graphql': 'GraphQL'
260
+ };
261
+
262
+ return Object.keys(filesByExtension)
263
+ .sort()
264
+ .map(ext => ({
265
+ extension: ext,
266
+ description: extensionDescriptions[ext] || 'Unknown',
267
+ count: filesByExtension[ext].length,
268
+ files: filesByExtension[ext]
269
+ }));
270
+ }
271
+
272
+ /**
273
+ * Calculate total statistics for scanned files
274
+ * @param {object} filesByExtension - Files grouped by extension
275
+ * @returns {object} Statistics object
276
+ */
277
+ calculateStatistics(filesByExtension) {
278
+ let totalFiles = 0;
279
+ let totalSize = 0;
280
+ const extensionCount = Object.keys(filesByExtension).length;
281
+
282
+ Object.values(filesByExtension).forEach(files => {
283
+ totalFiles += files.length;
284
+ totalSize += files.reduce((sum, file) => sum + file.size, 0);
285
+ });
286
+
287
+ return {
288
+ totalFiles,
289
+ totalSize,
290
+ extensionCount,
291
+ averageFileSize: totalFiles > 0 ? Math.round(totalSize / totalFiles) : 0,
292
+ totalSizeFormatted: this.formatFileSize(totalSize)
293
+ };
294
+ }
295
+
296
+ /**
297
+ * Format file size in human readable format
298
+ * @param {number} bytes - Size in bytes
299
+ * @returns {string} Formatted size string
300
+ */
301
+ formatFileSize(bytes) {
302
+ const units = ['B', 'KB', 'MB', 'GB'];
303
+ let size = bytes;
304
+ let unitIndex = 0;
305
+
306
+ while (size >= 1024 && unitIndex < units.length - 1) {
307
+ size /= 1024;
308
+ unitIndex++;
309
+ }
310
+
311
+ return `${size.toFixed(1)} ${units[unitIndex]}`;
312
+ }
313
+
314
+ /**
315
+ * Display scan results summary
316
+ * @param {object} filesByExtension - Files grouped by extension
317
+ */
318
+ displayScanSummary(filesByExtension) {
319
+ const stats = this.calculateStatistics(filesByExtension);
320
+ const extensions = Object.keys(filesByExtension).sort();
321
+
322
+ console.log(chalk.cyan('\nScan Summary:'));
323
+ console.log(chalk.gray(` Extensions found: ${extensions.join(', ')}`));
324
+ console.log(chalk.gray(` Total files: ${stats.totalFiles}`));
325
+ console.log(chalk.gray(` Total size: ${stats.totalSizeFormatted}`));
326
+ console.log();
327
+ }
328
+ }
329
+
330
+ export default Scanner;