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
Flexible filtering system using glob patterns and custom filter handlers to control which files and directories are included in comparisons.
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;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/**"
};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
});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);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);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
});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