Node JS directory compare library with extensive comparison options and TypeScript support
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
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)
});Install with Tessl CLI
npx tessl i tessl/npm-dir-compare