Source: ignoreProcessor.js

// src/ignoreProcessor.js

const fs = require('fs');
const path = require('path');
const ignore = require('ignore');

/**
 * A class that processes and manages ignore patterns from various ignore files (e.g., .gitignore, .npmignore).
 * Handles loading and applying ignore patterns from standard ignore files, custom ignore files, and directory-specific ignore files.
 *
 * @class
 * @example
 * const processor = new IgnoreProcessor(['/path/to/custom/ignore'], '/path/to/config.json');
 * if (processor.isIgnored('file/to/check.js')) {
 *   console.log('This file is ignored');
 * }
 *
 * @property {ignore} ig - Instance of the ignore package for pattern matching
 * @property {Array<string>} standardIgnoreFiles - List of standard ignore file names to look for
 */
class IgnoreProcessor {
  /**
   * Constructs an IgnoreProcessor instance.
   * @param {Array<string>} customIgnoreFiles - Array of custom ignore file paths.
   * @param {string} [configPath=null] - Path to a custom configuration file containing default ignore patterns.
   */
  constructor(customIgnoreFiles = [], configPath = null) {
    this.ig = ignore();
    this.standardIgnoreFiles = [
      '.gitignore',
      '.npmignore',
      '.eslintignore',
      '.dockerignore',
      '.prettierignore',
      '.hgignore',
      '.svnignore',
      // Add more standard ignore files as needed
    ];
    this.loadDefaultPatterns(configPath);
    this.loadCustomIgnoreFiles(customIgnoreFiles);
  }

  /**
   * Loads default ignore patterns from a JSON configuration file.
   * @param {string|null} configPath - Path to the JSON configuration file.
   */
  loadDefaultPatterns(configPath = null) {
    const defaultPatternsPath = configPath
      ? path.resolve(configPath)
      : path.join(__dirname, '../config/defaultIgnorePatterns.json');
    if (fs.existsSync(defaultPatternsPath)) {
      try {
        const data = JSON.parse(fs.readFileSync(defaultPatternsPath, 'utf-8'));
        if (Array.isArray(data.ignorePatterns)) {
          this.ig.add(data.ignorePatterns);
          console.log(`Loaded default ignore patterns from ${defaultPatternsPath}`);
        } else {
          console.warn(
            `Invalid format in default ignore patterns file: ${defaultPatternsPath}. Expected an array under "ignorePatterns".`
          );
        }
      } catch (error) {
        console.error(
          `Error parsing default ignore patterns file: ${defaultPatternsPath}\n${error.message}`
        );
      }
    } else {
      console.warn(
        `Default ignore patterns file not found at ${defaultPatternsPath}. Continuing without default patterns.`
      );
    }
  }

  /**
   * Loads and processes custom ignore files provided by the user.
   * @param {Array<string>} customIgnoreFiles - Array of custom ignore file paths.
   */
  loadCustomIgnoreFiles(customIgnoreFiles) {
    customIgnoreFiles.forEach((file) => {
      const absolutePath = path.isAbsolute(file) ? file : path.join(process.cwd(), file);
      if (fs.existsSync(absolutePath)) {
        try {
          const patterns = fs
            .readFileSync(absolutePath, 'utf-8')
            .split('\n')
            .map((line) => line.trim())
            .filter((line) => line && !line.startsWith('#')); // Remove empty lines and comments
          this.ig.add(patterns);
          console.log(`Loaded custom ignore patterns from ${absolutePath}`);
        } catch (error) {
          console.error(`Error reading custom ignore file: ${absolutePath}\n${error.message}`);
        }
      } else {
        console.warn(`Custom ignore file not found: ${absolutePath}`);
      }
    });
  }

  /**
   * Loads and processes ignore patterns from a specific directory's ignore files.
   * This method is used to handle ignore files in subdirectories during traversal.
   * @param {string} dirPath - Absolute path of the directory to check for ignore files.
   * @returns {Array<string>} - Array of ignore patterns found in the directory.
   */
  loadIgnorePatternsFromDirectory(dirPath) {
    const patterns = [];
    // Detect all .{{service}}ignore files in the directory
    const ignoreFiles = this.standardIgnoreFiles.filter((ignoreFile) =>
      fs.existsSync(path.join(dirPath, ignoreFile))
    );

    // Additionally, detect any other files that end with .ignore
    try {
      const dirEntries = fs.readdirSync(dirPath, { withFileTypes: true });
      dirEntries.forEach((entry) => {
        if (
          entry.isFile() &&
          entry.name.endsWith('.ignore') &&
          !this.standardIgnoreFiles.includes(entry.name)
        ) {
          ignoreFiles.push(entry.name);
        }
      });
    } catch (error) {
      console.error(
        `Error reading directory for additional ignore files: ${dirPath}\n${error.message}`
      );
    }

    ignoreFiles.forEach((ignoreFile) => {
      const ignoreFilePath = path.join(dirPath, ignoreFile);
      if (fs.existsSync(ignoreFilePath)) {
        try {
          const filePatterns = fs
            .readFileSync(ignoreFilePath, 'utf-8')
            .split('\n')
            .map((line) => line.trim())
            .filter((line) => line && !line.startsWith('#')); // Remove empty lines and comments
          // Prepend directory path to make patterns relative to rootDir
          const relativeDir = path.relative(process.cwd(), dirPath);
          const adjustedPatterns = filePatterns.map((pattern) => {
            if (pattern.startsWith('/')) {
              // Absolute patterns relative to the directory
              return path.join(relativeDir, pattern.substring(1));
            } else {
              // Relative patterns
              return path.join(relativeDir, pattern);
            }
          });
          patterns.push(...adjustedPatterns);
          console.log(`Loaded ignore patterns from ${ignoreFilePath}`);
        } catch (error) {
          console.error(`Error reading ignore file: ${ignoreFilePath}\n${error.message}`);
        }
      }
    });

    return patterns;
  }

  /**
   * Determines if a given file path should be ignored based on loaded patterns.
   * @param {string} filePath - Relative file path to check.
   * @returns {boolean} - Returns true if the file is ignored; otherwise, false.
   */
  isIgnored(filePath) {
    return this.ig.ignores(filePath);
  }

  /**
   * Adds additional patterns to the ignore instance.
   * Useful for dynamically adding patterns during traversal.
   * @param {Array<string>} patterns - Array of ignore patterns to add.
   */
  addPatterns(patterns) {
    this.ig.add(patterns);
  }
}

module.exports = IgnoreProcessor;