CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-dir-compare

Node JS directory compare library with extensive comparison options and TypeScript support

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

filters-and-patterns.mddocs/

Filters and Patterns

Flexible filtering system using glob patterns and custom filter handlers to control which files and directories are included in comparisons.

Capabilities

Built-in Filter Handlers

Collection of built-in filtering mechanisms for controlling comparison scope.

interface FilterHandlers {
  /**
   * Default minimatch-based glob filter.
   * Uses includeFilter and excludeFilter options with minimatch patterns.
   */
  defaultFilterHandler: FilterHandler;
}

/**
 * Filter handler function type
 * @param entry Filesystem entry to evaluate
 * @param relativePath Path relative to comparison root
 * @param options Comparison options
 * @returns true to include entry in comparison, false to exclude
 */
type FilterHandler = (entry: Entry, relativePath: string, options: Options) => boolean;

Glob Pattern Filtering

Default filtering using minimatch glob patterns with include and exclude options.

interface GlobFilterOptions extends Options {
  /**
   * Comma-separated minimatch patterns for files/directories to include.
   * Only entries matching these patterns will be compared.
   */
  includeFilter?: string;
  
  /**
   * Comma-separated minimatch patterns for files/directories to exclude.
   * Entries matching these patterns will be skipped.
   */
  excludeFilter?: string;
}

Basic Glob Pattern Usage:

import { compareSync } from "dir-compare";

// Exclude common build artifacts and version control
const basicFilter = {
  compareContent: true,
  excludeFilter: ".git,node_modules,*.log,build,dist"
};

const result1 = compareSync("/project1", "/project2", basicFilter);

// Include only specific file types
const sourceCodeOnly = {
  compareContent: true,
  includeFilter: "*.js,*.ts,*.json,*.md",
  excludeFilter: "*.test.js,*.spec.ts" // Exclude test files
};

const result2 = compareSync("/src1", "/src2", sourceCodeOnly);

// Complex patterns using minimatch syntax
const advancedPatterns = {
  compareContent: true,
  includeFilter: "src/**/*.{js,ts},docs/**/*.md,*.json",
  excludeFilter: "**/test/**,**/*.test.*,**/node_modules/**,**/.git/**"
};

const result3 = compareSync("/workspace1", "/workspace2", advancedPatterns);

Glob Pattern Examples:

// Pattern examples and their meanings
const patternExamples = {
  // Exact matches
  excludeFilter: ".git,package-lock.json",
  
  // Wildcard matches
  excludeFilter: "*.log,*.tmp",
  
  // Directory patterns
  excludeFilter: "node_modules,build,dist",
  
  // Path-based patterns
  excludeFilter: "/tests/expected,**/coverage/**",
  
  // Globstar patterns (match any depth)
  excludeFilter: "**/node_modules/**,**/*.log",
  
  // Multiple file extensions
  includeFilter: "*.{js,ts,jsx,tsx}",
  
  // Specific subdirectories
  includeFilter: "src/**/*.js,lib/**/*.ts",
  
  // Combined include/exclude
  includeFilter: "**/*.js",
  excludeFilter: "**/*.test.js,**/node_modules/**"
};

Custom Filter Handlers

Create specialized filtering logic for complex scenarios.

/**
 * Custom filter handler example for file size limits
 */
function createSizeFilter(maxSizeBytes: number): FilterHandler {
  return (entry: Entry, relativePath: string, options: Options): boolean => {
    // Always include directories for traversal
    if (entry.isDirectory) {
      return true;
    }
    
    // Filter files by size
    return entry.stat.size <= maxSizeBytes;
  };
}

/**
 * Custom filter handler example for file age
 */
function createAgeFilter(maxAgeMs: number): FilterHandler {
  const cutoffDate = new Date(Date.now() - maxAgeMs);
  
  return (entry: Entry, relativePath: string, options: Options): boolean => {
    // Include directories
    if (entry.isDirectory) {
      return true;
    }
    
    // Filter files by modification time
    return entry.stat.mtime >= cutoffDate;
  };
}

/**
 * Extension-based filter with case handling
 */
function createExtensionFilter(allowedExtensions: string[], ignoreCase = true): FilterHandler {
  const extensions = ignoreCase 
    ? allowedExtensions.map(ext => ext.toLowerCase())
    : allowedExtensions;
    
  return (entry: Entry, relativePath: string, options: Options): boolean => {
    if (entry.isDirectory) {
      return true;
    }
    
    const ext = path.extname(entry.name);
    const checkExt = ignoreCase ? ext.toLowerCase() : ext;
    
    return extensions.includes(checkExt);
  };
}

Usage with Custom Filters:

import { compareSync, filterHandlers } from "dir-compare";
import path from "path";

// Combine size and extension filtering
const sizeFilter = createSizeFilter(10 * 1024 * 1024); // 10MB limit
const jsFilter = createExtensionFilter(['.js', '.ts', '.json']);

// Composite filter combining multiple criteria
const compositeFilter: FilterHandler = (entry, relativePath, options) => {
  // Apply default glob filtering first
  if (!filterHandlers.defaultFilterHandler(entry, relativePath, options)) {
    return false;
  }
  
  // Apply size filter
  if (!sizeFilter(entry, relativePath, options)) {
    return false;
  }
  
  // Apply extension filter
  return jsFilter(entry, relativePath, options);
};

const result = compareSync("/large-project1", "/large-project2", {
  compareContent: true,
  excludeFilter: "node_modules,*.log",
  filterHandler: compositeFilter
});

// Age-based filtering for recent changes
const recentChangesFilter = createAgeFilter(7 * 24 * 60 * 60 * 1000); // 7 days

const recentResult = compareSync("/backup", "/current", {
  compareContent: true,
  compareDate: true,
  filterHandler: recentChangesFilter
});

.gitignore-style Filtering

Example implementation for .gitignore-compatible filtering.

/**
 * Creates a filter handler that respects .gitignore rules
 * (This is a conceptual example - actual implementation would use globby or similar)
 */
function createGitignoreFilter(rootPath1: string, rootPath2: string): FilterHandler {
  // Load .gitignore files from both directories
  const loadGitignoreRules = (rootPath: string): string[] => {
    try {
      const gitignorePath = path.join(rootPath, '.gitignore');
      const content = fs.readFileSync(gitignorePath, 'utf8');
      return content.split('\n')
        .map(line => line.trim())
        .filter(line => line && !line.startsWith('#'));
    } catch {
      return [];
    }
  };
  
  const rules1 = loadGitignoreRules(rootPath1);
  const rules2 = loadGitignoreRules(rootPath2);
  const allRules = [...new Set([...rules1, ...rules2])];
  
  return (entry: Entry, relativePath: string, options: Options): boolean => {
    // Apply default filtering first
    if (!filterHandlers.defaultFilterHandler(entry, relativePath, options)) {
      return false;
    }
    
    // Check against gitignore rules
    const entryPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
    
    for (const rule of allRules) {
      // Simplified gitignore matching (real implementation would be more complex)
      if (minimatch(entryPath, rule)) {
        return false;
      }
    }
    
    return true;
  };
}

Usage:

import { compareSync } from "dir-compare";

// Use gitignore-style filtering
const gitOptions = {
  compareContent: true,
  compareSize: true,
  filterHandler: createGitignoreFilter("/repo1", "/repo2"),
  // Regular filters still apply after gitignore rules
  excludeFilter: "*.tmp",
  includeFilter: "**/*" // Include all files not filtered by gitignore
};

const gitResult = compareSync("/repo1", "/repo2", gitOptions);

Performance-Optimized Filtering

import { compare, filterHandlers } from "dir-compare";

// Early directory pruning for performance
const performanceFilter: FilterHandler = (entry, relativePath, options) => {
  // Skip large directories early
  const skipDirs = ['node_modules', '.git', 'build', 'dist', 'coverage'];
  if (entry.isDirectory && skipDirs.includes(entry.name)) {
    return false;
  }
  
  // Skip large files early
  if (!entry.isDirectory && entry.stat.size > 100 * 1024 * 1024) { // 100MB
    return false;
  }
  
  // Apply default filtering for other cases
  return filterHandlers.defaultFilterHandler(entry, relativePath, options);
};

// Efficient large directory comparison
const efficientOptions = {
  compareSize: true,        // Fast size comparison first
  compareContent: false,    // Skip slow content comparison
  skipEmptyDirs: true,      // Skip empty directories
  filterHandler: performanceFilter,
  excludeFilter: "*.iso,*.dmg,*.zip,*.tar,*.gz" // Skip archives
};

const largeResult = await compare("/large-dir1", "/large-dir2", efficientOptions);

Filter Chain Pattern

import { FilterHandler } from "dir-compare";

/**
 * Creates a filter chain that applies multiple filters in sequence
 */
function createFilterChain(...filters: FilterHandler[]): FilterHandler {
  return (entry: Entry, relativePath: string, options: Options): boolean => {
    return filters.every(filter => filter(entry, relativePath, options));
  };
}

// Example filter chain
const comprehensiveFilter = createFilterChain(
  filterHandlers.defaultFilterHandler,           // Apply glob patterns
  createSizeFilter(50 * 1024 * 1024),           // 50MB size limit
  createAgeFilter(30 * 24 * 60 * 60 * 1000),    // 30 days age limit
  createExtensionFilter(['.js', '.ts', '.json', '.md']) // Specific extensions
);

const chainResult = compareSync("/filtered1", "/filtered2", {
  compareContent: true,
  includeFilter: "src/**,docs/**",
  excludeFilter: "**/*.test.*",
  filterHandler: comprehensiveFilter
});

Debugging Filter Behavior

import { compareSync, FilterHandler } from "dir-compare";

// Logging filter for debugging what gets included/excluded
function createLoggingFilter(baseFilter: FilterHandler): FilterHandler {
  return (entry: Entry, relativePath: string, options: Options): boolean => {
    const result = baseFilter(entry, relativePath, options);
    const entryPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
    
    console.log(`Filter ${result ? 'INCLUDE' : 'EXCLUDE'}: ${entryPath} (${entry.isDirectory ? 'dir' : 'file'})`);
    
    return result;
  };
}

// Debug filtering behavior
const debugFilter = createLoggingFilter(filterHandlers.defaultFilterHandler);

const debugResult = compareSync("/debug1", "/debug2", {
  compareContent: true,
  excludeFilter: "*.log,temp/**",
  filterHandler: debugFilter
});

Install with Tessl CLI

npx tessl i tessl/npm-dir-compare

docs

comparison-functions.md

extension-points.md

file-comparators.md

filters-and-patterns.md

index.md

tile.json