Source: fileScanner.js

const fs = require('fs').promises;
const path = require('path');

/**
 * A class for scanning directories and filtering files based on extensions and ignore patterns.
 * Supports common version control system ignore files and custom ignore patterns.
 * Provides functionality to recursively traverse directories while respecting ignore rules.
 *
 * @class
 * @classdesc Scans file system directories while respecting ignore patterns and file extension filters.
 * @example
 * const scanner = new FileScanner('/path/to/root', ignoreProcessor);
 * const files = await scanner.scan();
 *
 * @property {string} rootDir - The root directory to start scanning from
 * @property {IgnoreProcessor} ignoreProcessor - Processor for handling ignore patterns
 * @property {Array<string>} allowedExtensions - List of file extensions to include in scan results
 *
 * @see {@link IgnoreProcessor} for ignore pattern handling
 */
class FileScanner {
  /**
   * Constructs a FileScanner instance.
   * @param {string} rootDir - Root directory to start scanning from.
   * @param {IgnoreProcessor} ignoreProcessor - Instance of IgnoreProcessor to handle ignore patterns.
   * @param {Array<string>} [additionalExtensions=[]] - Additional file extensions to include.
   */
  constructor(rootDir, ignoreProcessor, additionalExtensions = []) {
    this.rootDir = rootDir;
    this.ignoreProcessor = ignoreProcessor;
    this.allowedExtensions = [
      '.js',
      '.jsx',
      '.ts',
      '.tsx',
      '.json',
      '.md',
      '.html',
      '.css',
      '.py',
      '.java',
      '.c',
      '.cpp',
      '.rb',
      '.go',
      '.php',
      '.sh',
      ...additionalExtensions,
    ];
  }

  /**
   * Initiates the scanning process.
   * @returns {Promise<Array<string>>} - Array of relative file paths that are not ignored.
   */
  async scan() {
    const files = [];
    await this.traverseDirectory(this.rootDir, files, []);
    return files;
  }

  /**
   * Recursively traverses directories to find eligible files.
   * @param {string} currentDir - Current directory being traversed.
   * @param {Array<string>} files - Accumulator for eligible file paths.
   * @param {Array<string>} parentIgnorePatterns - Accumulated ignore patterns from parent directories.
   */
  async traverseDirectory(currentDir, files, parentIgnorePatterns) {
    let currentIgnorePatterns = [...parentIgnorePatterns];

    // Load ignore patterns from the current directory's ignore files
    const dirIgnorePatterns = this.ignoreProcessor.loadIgnorePatternsFromDirectory(currentDir);
    currentIgnorePatterns.push(...dirIgnorePatterns);

    // Update the IgnoreProcessor with new patterns
    if (dirIgnorePatterns.length > 0) {
      this.ignoreProcessor.addPatterns(dirIgnorePatterns);
    }

    // Create a new ignore instance for the current directory
    const ig = this.ignoreProcessor.ig;

    // Read directory entries
    let entries;
    try {
      entries = await fs.readdir(currentDir, { withFileTypes: true });
    } catch (error) {
      console.error(`Error reading directory: ${currentDir}\n${error.message}`);
      return;
    }

    // Sort entries alphabetically to ensure consistent order
    entries.sort((a, b) => a.name.localeCompare(b.name));

    for (const entry of entries) {
      const fullPath = path.join(currentDir, entry.name);
      const relativePath = path.relative(this.rootDir, fullPath).split(path.sep).join('/'); // Normalize to forward slashes

      // Skip the current directory's ignore files as they have already been processed
      if (
        this.ignoreProcessor.standardIgnoreFiles.includes(entry.name) &&
        currentDir === this.rootDir
      ) {
        continue;
      }

      // Determine if the entry should be ignored based on current ignore patterns
      if (ig.ignores(relativePath)) {
        continue;
      }

      if (entry.isDirectory()) {
        // Exclude directories that are commonly ignored regardless of ignore files
        if (this.isCommonIgnoredDirectory(entry.name)) {
          continue;
        }
        await this.traverseDirectory(fullPath, files, currentIgnorePatterns);
      } else if (entry.isFile()) {
        if (this.isAllowedExtension(entry.name)) {
          files.push(relativePath);
        }
      }
    }
  }

  /**
   * Checks if the file has an allowed extension.
   * @param {string} filename - Name of the file to check.
   * @returns {boolean} - Returns true if the file has an allowed extension; otherwise, false.
   */
  isAllowedExtension(filename) {
    const ext = path.extname(filename).toLowerCase();
    return this.allowedExtensions.includes(ext);
  }

  /**
   * Checks if the directory name is commonly ignored (e.g., node_modules, .git).
   * @param {string} dirname - Name of the directory to check.
   * @returns {boolean} - Returns true if the directory is commonly ignored; otherwise, false.
   */
  isCommonIgnoredDirectory(dirname) {
    const ignoredDirs = [
      'node_modules',
      '.git',
      '.svn',
      '.hg',
      'dist',
      'build',
      'coverage',
      'temp',
      'cache',
      'package-lock.json',
      // Add more commonly ignored directories as needed
    ];
    return ignoredDirs.includes(dirname);
  }
}

module.exports = FileScanner;