Comprehensive extension system allowing custom result builders, name comparators, and specialized comparison logic for advanced use cases.
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);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
});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);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)
});