Node.js Directory Comparison Utility
dir-compare is a Node.js library for comparing the contents and structure of two directories. It provides both synchronous (`compareSync`) and asynchronous (`compare`) comparison methods, supporting various strategies like size, content, and date comparison, along with advanced filtering options including glob patterns and `.gitignore` rules. The current stable version is 5.0.0, with regular updates addressing features, performance, and bug fixes. Key differentiators include its TypeScript support (since v4.0.0), significant performance improvements for large directory structures (e.g., 3x reduced heap usage and 2x faster content comparison since v4.0.0), and flexible extension points for custom comparators and result builders. The command-line interface (CLI) was moved to a separate package, `dir-compare-cli`, in v3.0.0.
Common errors
-
TypeError: dircompare.compare is not a function
cause Attempting to call `dircompare.compare` directly after a CommonJS `require('dir-compare')` on versions 4.0.0+ when the library might expose named exports more prominently, or if trying to call an async function synchronously.fixFor CommonJS, try `const { compare, compareSync } = require('dir-compare');` or for modern ES Modules: `import { compare, compareSync } from 'dir-compare';`. Ensure you are calling `compare` (async) with `.then()`/`await` or `compareSync` (sync) appropriately. -
Error: EACCES: permission denied, open 'path/to/file'
cause The Node.js process does not have sufficient read permissions for one or both of the directories or files being compared.fixEnsure the user running the Node.js application has read and execute permissions on all directories and files within the comparison paths. Alternatively, `dir-compare` added support for handling permission denied errors in v3.2.0; investigate options for graceful error handling within the comparison process if appropriate. -
TypeError: Cannot read properties of undefined (reading 'diffSet')
cause This usually happens if the `compare` or `compareSync` function failed to execute or returned an unexpected value (e.g., `null` or `undefined`) instead of a `Result` object, and subsequent code tries to access properties like `diffSet`.fixWrap the comparison call in a `try...catch` block (for sync) or add a `.catch()` handler (for async) to properly handle potential errors during the comparison process. Also, verify that the input paths are valid and accessible.
Warnings
- breaking The `skipSubdirs` option now behaves slightly differently, potentially affecting how comparisons handle subdirectories. Review issue #77 for specifics.
- breaking When using `dir-compare` to compare two individual files (not directories), the names of the files are now ignored in the comparison. This primarily affects scenarios where `dir-compare` was used for direct file-to-file comparison and relied on name matching.
- breaking The project was switched to TypeScript in v4.0.0. While existing JavaScript usage generally remains compatible, direct imports for types (`Options`, `Result`) are now available, and the internal structure is type-checked. This might implicitly affect type inference in certain editors for existing JavaScript projects.
- breaking The command-line interface (CLI) utility was extracted into a separate package, `dir-compare-cli`. The `dir-compare` package now only provides the library API.
- gotcha The `origin` field was added to the `Entry` interface (within `diffSet`) to distinguish whether an entry originated from the left or right directory. This provides more granular information but requires updating code that processed `diffSet` entries if `origin` is now relevant.
- gotcha Since v4.1.0, the library offers enhanced glob filter capabilities and the ability to implement `.gitignore` rules. If custom filtering logic was previously implemented manually, these new features can simplify the code.
Install
-
npm install dir-compare -
yarn add dir-compare -
pnpm add dir-compare
Imports
- compare
const dircompare = require('dir-compare'); dircompare.compare(...);import { compare } from 'dir-compare'; - compareSync
const dircompare = require('dir-compare'); dircompare.compareSync(...);import { compareSync } from 'dir-compare'; - Options
import { Options } from 'dir-compare'; - Result
import { Result } from 'dir-compare';
Quickstart
import { compare, compareSync, Options, Result } from 'dir-compare';
import * as path from 'path';
import * * as fs from 'fs';
// Create dummy directories and files for demonstration
const dir1 = path.join(__dirname, 'test-dir1');
const dir2 = path.join(__dirname, 'test-dir2');
fs.mkdirSync(dir1, { recursive: true });
fs.mkdirSync(dir2, { recursive: true });
fs.writeFileSync(path.join(dir1, 'fileA.txt'), 'Content A');
fs.writeFileSync(path.join(dir1, 'fileB.txt'), 'Content B');
fs.writeFileSync(path.join(dir2, 'fileA.txt'), 'Content A');
fs.writeFileSync(path.join(dir2, 'fileC.txt'), 'Content C');
const options: Options = { compareSize: true, compareContent: true, excludeFilter: 'fileB.txt' };
console.log('--- Synchronous Comparison ---');
try {
const resSync: Result = compareSync(dir1, dir2, options);
console.log('Directories are %s', resSync.same ? 'identical' : 'different');
console.log('Equal entries: %s, Distinct entries: %s, Left only: %s, Right only: %s',
resSync.equal, resSync.distinct, resSync.left, resSync.right);
resSync.diffSet.forEach(dif => {
console.log(`- Path: ${dif.relativePath}, Name1: ${dif.name1}, Name2: ${dif.name2}, State: ${dif.state}`);
});
} catch (error) {
console.error('Synchronous comparison failed:', error);
}
console.log('\n--- Asynchronous Comparison ---');
compare(dir1, dir2, options)
.then(resAsync => {
console.log('Directories are %s', resAsync.same ? 'identical' : 'different');
console.log('Equal entries: %s, Distinct entries: %s, Left only: %s, Right only: %s',
resAsync.equal, resAsync.distinct, resAsync.left, resAsync.right);
resAsync.diffSet.forEach(dif => {
console.log(`- Path: ${dif.relativePath}, Name1: ${dif.name1}, Name2: ${dif.name2}, State: ${dif.state}`);
});
})
.catch(error => console.error('Asynchronous comparison failed:', error))
.finally(() => {
// Cleanup dummy directories
fs.rmSync(dir1, { recursive: true, force: true });
fs.rmSync(dir2, { recursive: true, force: true });
});