or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

comparison-functions.mdextension-points.mdfile-comparators.mdfilters-and-patterns.mdindex.md
tile.json

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
});