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

extension-points.mddocs/

Extension Points

Comprehensive extension system allowing custom result builders, name comparators, and specialized comparison logic for advanced use cases.

Capabilities

Result Builder Extension Point

Custom result building for specialized statistics collection and difference processing.

/**
 * Extension point for custom result building.
 * Called for each compared entry pair to update statistics and diffSet.
 * @param entry1 Left entry (undefined if missing)
 * @param entry2 Right entry (undefined if missing)  
 * @param state Comparison state
 * @param level Directory depth level
 * @param relativePath Path relative to comparison root
 * @param options Comparison options
 * @param statistics Statistics object to update
 * @param diffSet Difference array to append to (undefined if noDiffSet is true)
 * @param reason Reason for distinction (undefined if entries are equal)
 * @param permissionDeniedState Permission access state
 */
type ResultBuilder = (
  entry1: Entry | undefined, 
  entry2: Entry | undefined, 
  state: DifferenceState, 
  level: number,
  relativePath: string, 
  options: Options, 
  statistics: InitialStatistics, 
  diffSet: DiffSet | undefined,
  reason: Reason | undefined, 
  permissionDeniedState: PermissionDeniedState
) => void;

Custom Result Builder Examples:

import { compareSync, ResultBuilder, InitialStatistics } from "dir-compare";

// Enhanced statistics collector
const enhancedResultBuilder: ResultBuilder = (
  entry1, entry2, state, level, relativePath, options, statistics, diffSet, reason, permissionDeniedState
) => {
  // Call default result builder first
  const defaultBuilder = options.defaultResultBuilder || ((e1, e2, s, l, rp, opts, stats, ds, r, pds) => {
    // Default implementation logic would go here
    // This is simplified - the actual default is more complex
    switch (s) {
      case 'equal':
        stats.equal++;
        if (e1?.isDirectory) stats.equalDirs++;
        else stats.equalFiles++;
        break;
      case 'distinct':
        stats.distinct++;
        if (e1?.isDirectory) stats.distinctDirs++;
        else stats.distinctFiles++;
        break;
      case 'left':
        stats.left++;
        if (e1?.isDirectory) stats.leftDirs++;
        else stats.leftFiles++;
        break;
      case 'right':
        stats.right++;
        if (e2?.isDirectory) stats.rightDirs++;
        else stats.rightFiles++;
        break;
    }
  });
  
  defaultBuilder(entry1, entry2, state, level, relativePath, options, statistics, diffSet, reason, permissionDeniedState);
  
  // Add custom statistics
  if (!statistics.customStats) {
    statistics.customStats = {
      maxDepth: 0,
      largeFiles: 0,
      sizeDifferences: 0,
      contentDifferences: 0
    };
  }
  
  // Track maximum depth
  statistics.customStats.maxDepth = Math.max(statistics.customStats.maxDepth, level);
  
  // Track large files (>10MB)
  if (entry1 && !entry1.isDirectory && entry1.stat.size > 10 * 1024 * 1024) {
    statistics.customStats.largeFiles++;
  }
  if (entry2 && !entry2.isDirectory && entry2.stat.size > 10 * 1024 * 1024) {
    statistics.customStats.largeFiles++;
  }
  
  // Track specific difference types
  if (reason === 'different-size') {
    statistics.customStats.sizeDifferences++;
  }
  if (reason === 'different-content') {
    statistics.customStats.contentDifferences++;
  }
  
  // Add custom diff entry with additional metadata
  if (diffSet && state !== 'equal') {
    const customDiff = {
      path1: entry1?.path,
      path2: entry2?.path,
      relativePath,
      name1: entry1?.name,
      name2: entry2?.name,
      state,
      permissionDeniedState,
      type1: entry1 ? (entry1.isDirectory ? 'directory' : 'file') : 'missing',
      type2: entry2 ? (entry2.isDirectory ? 'directory' : 'file') : 'missing',
      size1: entry1?.stat.size,
      size2: entry2?.stat.size,
      date1: entry1?.stat.mtime,
      date2: entry2?.stat.mtime,
      level,
      reason,
      // Custom fields
      sizeDifference: entry1 && entry2 ? Math.abs(entry1.stat.size - entry2.stat.size) : undefined,
      isLargeFile: (entry1?.stat.size || 0) > 10 * 1024 * 1024 || (entry2?.stat.size || 0) > 10 * 1024 * 1024
    };
    
    diffSet.push(customDiff as any); // Cast needed due to custom fields
  }
};

// Usage with enhanced result builder
const result = compareSync("/dir1", "/dir2", {
  compareContent: true,
  resultBuilder: enhancedResultBuilder
});

console.log("Custom statistics:", result.customStats);

Name Comparator Extension Point

Custom name comparison logic for specialized sorting and matching requirements.

/**
 * Extension point for custom name comparison.
 * @param name1 Left entry name
 * @param name2 Right entry name  
 * @param options Comparison options
 * @returns 0 if names are equal, -1 if name1 < name2, 1 if name1 > name2
 */
type CompareNameHandler = (name1: string, name2: string, options: Options) => 0 | 1 | -1;

interface CompareNameHandlers {
  /**
   * Default strcmp-based name comparator.
   * Handles case sensitivity based on ignoreCase option.
   */
  defaultNameCompare: CompareNameHandler;
}

Custom Name Comparator Examples:

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

// Ignore file extensions when comparing names
const ignoreExtensionCompare: CompareNameHandler = (name1, name2, options) => {
  let baseName1 = path.basename(name1, path.extname(name1));
  let baseName2 = path.basename(name2, path.extname(name2));
  
  if (options.ignoreCase) {
    baseName1 = baseName1.toLowerCase();
    baseName2 = baseName2.toLowerCase();
  }
  
  return baseName1 === baseName2 ? 0 : (baseName1 > baseName2 ? 1 : -1);
};

// Semantic version comparison for package files
const semverCompare: CompareNameHandler = (name1, name2, options) => {
  const versionRegex = /^(.+?)-(\d+\.\d+\.\d+.*?)(\.[^.]+)?$/;
  
  const match1 = name1.match(versionRegex);
  const match2 = name2.match(versionRegex);
  
  // If both match version pattern, compare semantically
  if (match1 && match2 && match1[1] === match2[1]) {
    const version1 = match1[2];
    const version2 = match2[2];
    
    // Simplified semver comparison (real implementation would use semver library)
    const parts1 = version1.split('.').map(Number);
    const parts2 = version2.split('.').map(Number);
    
    for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
      const v1 = parts1[i] || 0;
      const v2 = parts2[i] || 0;
      if (v1 !== v2) return v1 > v2 ? 1 : -1;
    }
    return 0;
  }
  
  // Fall back to default comparison
  return compareNameHandlers.defaultNameCompare(name1, name2, options);
};

// Numeric comparison for numbered files
const numericCompare: CompareNameHandler = (name1, name2, options) => {
  const numRegex = /^(.+?)(\d+)(.*)$/;
  const match1 = name1.match(numRegex);
  const match2 = name2.match(numRegex);
  
  // If both have numeric parts and same prefix/suffix, compare numerically
  if (match1 && match2 && match1[1] === match2[1] && match1[3] === match2[3]) {
    const num1 = parseInt(match1[2]);
    const num2 = parseInt(match2[2]);
    return num1 === num2 ? 0 : (num1 > num2 ? 1 : -1);
  }
  
  return compareNameHandlers.defaultNameCompare(name1, name2, options);
};

Usage with Custom Name Comparators:

// Compare ignoring file extensions
const extensionIgnoreResult = compareSync("/docs1", "/docs2", {
  compareContent: true,
  compareNameHandler: ignoreExtensionCompare,
  ignoreExtension: true // Custom option for the comparator
});

// Compare package versions semantically
const packageResult = compareSync("/packages1", "/packages2", {
  compareSize: true,
  compareNameHandler: semverCompare,
  includeFilter: "*-[0-9]*.*" // Match versioned files
});

// Compare numbered files correctly
const numberedResult = compareSync("/sequence1", "/sequence2", {
  compareContent: true,
  compareNameHandler: numericCompare
});

Combining Extension Points

Advanced usage combining multiple extension points for specialized comparison scenarios.

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

// Comprehensive custom comparison system
class CustomComparisonSystem {
  private options: any = {};
  
  createNameComparator(): CompareNameHandler {
    return (name1, name2, options) => {
      // Apply custom name logic based on file type
      if (this.isVersionedFile(name1) && this.isVersionedFile(name2)) {
        return this.compareVersionedNames(name1, name2);
      }
      return compareNameHandlers.defaultNameCompare(name1, name2, options);
    };
  }
  
  createResultBuilder(): ResultBuilder {
    return (entry1, entry2, state, level, relativePath, options, statistics, diffSet, reason, permissionDeniedState) => {
      // Collect specialized statistics
      this.updateCustomStatistics(statistics, entry1, entry2, state, reason);
      
      // Apply default result building
      this.applyDefaultResultBuilding(entry1, entry2, state, level, relativePath, options, statistics, diffSet, reason, permissionDeniedState);
      
      // Add custom diff processing
      this.processCustomDifferences(diffSet, entry1, entry2, state, relativePath, level, reason);
    };
  }
  
  createFileComparator(): CompareFileHandler {
    return {
      compareSync: (path1, stat1, path2, stat2, options) => {
        // Custom file comparison logic
        return this.compareFilesSync(path1, stat1, path2, stat2, options);
      },
      compareAsync: async (path1, stat1, path2, stat2, options) => {
        return this.compareFilesAsync(path1, stat1, path2, stat2, options);
      }
    };
  }
  
  createFilter(): FilterHandler {
    return (entry, relativePath, options) => {
      // Custom filtering logic
      return this.shouldIncludeEntry(entry, relativePath, options);
    };
  }
  
  // Helper methods
  private isVersionedFile(name: string): boolean {
    return /^.+-\d+\.\d+\.\d+/.test(name);
  }
  
  private compareVersionedNames(name1: string, name2: string): 0 | 1 | -1 {
    // Implement semantic version comparison
    return 0; // Simplified
  }
  
  private updateCustomStatistics(statistics: any, entry1: any, entry2: any, state: string, reason: any) {
    // Update custom statistics
    if (!statistics.customAnalysis) {
      statistics.customAnalysis = {
        versionedFiles: 0,
        configFiles: 0,
        documentFiles: 0
      };
    }
    
    if (entry1?.name.includes('config') || entry2?.name.includes('config')) {
      statistics.customAnalysis.configFiles++;
    }
  }
  
  private applyDefaultResultBuilding(entry1: any, entry2: any, state: string, level: number, relativePath: string, options: any, statistics: any, diffSet: any[], reason: any, permissionDeniedState: any) {
    // Apply standard result building logic
  }
  
  private processCustomDifferences(diffSet: any[], entry1: any, entry2: any, state: string, relativePath: string, level: number, reason: any) {
    // Process differences with custom logic
  }
  
  private compareFilesSync(path1: string, stat1: any, path2: string, stat2: any, options: any): boolean {
    // Custom file comparison
    return fileCompareHandlers.defaultFileCompare.compareSync(path1, stat1, path2, stat2, options);
  }
  
  private async compareFilesAsync(path1: string, stat1: any, path2: string, stat2: any, options: any): Promise<boolean> {
    // Custom async file comparison
    return fileCompareHandlers.defaultFileCompare.compareAsync(path1, stat1, path2, stat2, options);
  }
  
  private shouldIncludeEntry(entry: any, relativePath: string, options: any): boolean {
    // Custom inclusion logic
    return filterHandlers.defaultFilterHandler(entry, relativePath, options);
  }
}

// Usage of comprehensive custom system
const customSystem = new CustomComparisonSystem();

const comprehensiveResult = compareSync("/complex1", "/complex2", {
  compareContent: true,
  compareSize: true,
  compareDate: true,
  compareNameHandler: customSystem.createNameComparator(),
  resultBuilder: customSystem.createResultBuilder(),
  compareFileSync: customSystem.createFileComparator().compareSync,
  compareFileAsync: customSystem.createFileComparator().compareAsync,
  filterHandler: customSystem.createFilter()
});

console.log("Custom analysis:", comprehensiveResult.customAnalysis);

Extension Point Debugging and Testing

import { compareSync, ResultBuilder, CompareNameHandler } from "dir-compare";

// Debugging wrapper for extension points
function createDebuggingResultBuilder(baseBuilder?: ResultBuilder): ResultBuilder {
  return (entry1, entry2, state, level, relativePath, options, statistics, diffSet, reason, permissionDeniedState) => {
    console.log(`ResultBuilder called:`, {
      state,
      level,
      relativePath,
      name1: entry1?.name,
      name2: entry2?.name,
      reason,
      permissionDeniedState
    });
    
    if (baseBuilder) {
      baseBuilder(entry1, entry2, state, level, relativePath, options, statistics, diffSet, reason, permissionDeniedState);
    }
  };
}

function createDebuggingNameComparator(baseComparator: CompareNameHandler): CompareNameHandler {
  return (name1, name2, options) => {
    const result = baseComparator(name1, name2, options);
    console.log(`NameComparator: "${name1}" vs "${name2}" = ${result}`);
    return result;
  };
}

// Test extension points with debugging
const debugResult = compareSync("/test1", "/test2", {
  compareContent: true,
  resultBuilder: createDebuggingResultBuilder(),
  compareNameHandler: createDebuggingNameComparator(compareNameHandlers.defaultNameCompare)
});