diffx-js 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib.js +334 -0
- package/package.json +3 -2
- package/test.js +68 -1
package/lib.js
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js API wrapper for diffx CLI tool
|
|
3
|
+
*
|
|
4
|
+
* This module provides a JavaScript API for the diffx CLI tool,
|
|
5
|
+
* allowing you to compare structured data files programmatically.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { spawn } = require('child_process');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const { writeFileSync, mkdtempSync, rmSync } = require('fs');
|
|
12
|
+
const { tmpdir } = require('os');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {'json'|'yaml'|'toml'|'xml'|'ini'|'csv'} Format
|
|
16
|
+
* @typedef {'cli'|'json'|'yaml'|'unified'} OutputFormat
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Options for diff operations
|
|
21
|
+
* @typedef {Object} DiffOptions
|
|
22
|
+
* @property {Format} [format] - Input file format
|
|
23
|
+
* @property {OutputFormat} [output] - Output format
|
|
24
|
+
* @property {boolean} [recursive=false] - Compare directories recursively
|
|
25
|
+
* @property {string} [path] - Filter differences by path
|
|
26
|
+
* @property {string} [ignoreKeysRegex] - Ignore keys matching regex
|
|
27
|
+
* @property {number} [epsilon] - Tolerance for float comparisons
|
|
28
|
+
* @property {string} [arrayIdKey] - Key to use for array element identification
|
|
29
|
+
* @property {boolean} [optimize=false] - Enable memory optimization
|
|
30
|
+
* @property {number} [context] - Number of context lines in unified output
|
|
31
|
+
* @property {boolean} [ignoreWhitespace=false] - Ignore whitespace differences
|
|
32
|
+
* @property {boolean} [ignoreCase=false] - Ignore case differences
|
|
33
|
+
* @property {boolean} [quiet=false] - Suppress output (exit code only)
|
|
34
|
+
* @property {boolean} [brief=false] - Show only filenames
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Result of a diff operation
|
|
39
|
+
* @typedef {Object} DiffResult
|
|
40
|
+
* @property {string} type - Type of difference ('Added', 'Removed', 'Modified', 'TypeChanged')
|
|
41
|
+
* @property {string} path - Path to the changed element
|
|
42
|
+
* @property {*} [oldValue] - Old value (for Modified/TypeChanged)
|
|
43
|
+
* @property {*} [newValue] - New value (for Modified/TypeChanged/Added)
|
|
44
|
+
* @property {*} [value] - Value (for Removed)
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Error thrown when diffx command fails
|
|
49
|
+
*/
|
|
50
|
+
class DiffError extends Error {
|
|
51
|
+
constructor(message, exitCode, stderr) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = 'DiffError';
|
|
54
|
+
this.exitCode = exitCode;
|
|
55
|
+
this.stderr = stderr;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get the path to the diffx binary
|
|
61
|
+
* @returns {string} Path to diffx binary
|
|
62
|
+
*/
|
|
63
|
+
function getDiffxBinaryPath() {
|
|
64
|
+
// Check if local binary exists (installed via postinstall)
|
|
65
|
+
const binaryName = process.platform === 'win32' ? 'diffx.exe' : 'diffx';
|
|
66
|
+
const localBinaryPath = path.join(__dirname, 'bin', binaryName);
|
|
67
|
+
|
|
68
|
+
if (fs.existsSync(localBinaryPath)) {
|
|
69
|
+
return localBinaryPath;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Fall back to system PATH
|
|
73
|
+
return 'diffx';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Execute diffx command
|
|
78
|
+
* @param {string[]} args - Command arguments
|
|
79
|
+
* @returns {Promise<{stdout: string, stderr: string}>} Command output
|
|
80
|
+
*/
|
|
81
|
+
function executeDiffx(args) {
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const diffxPath = getDiffxBinaryPath();
|
|
84
|
+
|
|
85
|
+
const child = spawn(diffxPath, args, {
|
|
86
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
let stdout = '';
|
|
90
|
+
let stderr = '';
|
|
91
|
+
|
|
92
|
+
child.stdout.on('data', (data) => {
|
|
93
|
+
stdout += data.toString();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
child.stderr.on('data', (data) => {
|
|
97
|
+
stderr += data.toString();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
child.on('close', (code) => {
|
|
101
|
+
if (code === 0 || code === 1) {
|
|
102
|
+
// Exit code 1 means differences found, which is expected
|
|
103
|
+
resolve({ stdout, stderr });
|
|
104
|
+
} else {
|
|
105
|
+
reject(new DiffError(
|
|
106
|
+
`diffx exited with code ${code}`,
|
|
107
|
+
code,
|
|
108
|
+
stderr
|
|
109
|
+
));
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
child.on('error', (err) => {
|
|
114
|
+
if (err.code === 'ENOENT') {
|
|
115
|
+
reject(new DiffError(
|
|
116
|
+
'diffx command not found. Please install diffx CLI tool.',
|
|
117
|
+
-1,
|
|
118
|
+
''
|
|
119
|
+
));
|
|
120
|
+
} else {
|
|
121
|
+
reject(new DiffError(err.message, -1, ''));
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Compare two files or directories using diffx
|
|
129
|
+
*
|
|
130
|
+
* @param {string} input1 - Path to first file/directory or '-' for stdin
|
|
131
|
+
* @param {string} input2 - Path to second file/directory
|
|
132
|
+
* @param {DiffOptions} [options={}] - Comparison options
|
|
133
|
+
* @returns {Promise<string|DiffResult[]>} String output for CLI format, or array of DiffResult for JSON format
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* // Basic comparison
|
|
137
|
+
* const result = await diff('file1.json', 'file2.json');
|
|
138
|
+
* console.log(result);
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* // JSON output format
|
|
142
|
+
* const jsonResult = await diff('config1.yaml', 'config2.yaml', {
|
|
143
|
+
* format: 'yaml',
|
|
144
|
+
* output: 'json'
|
|
145
|
+
* });
|
|
146
|
+
* for (const diffItem of jsonResult) {
|
|
147
|
+
* console.log(diffItem);
|
|
148
|
+
* }
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* // Directory comparison with filtering
|
|
152
|
+
* const dirResult = await diff('dir1/', 'dir2/', {
|
|
153
|
+
* recursive: true,
|
|
154
|
+
* path: 'config',
|
|
155
|
+
* ignoreCase: true,
|
|
156
|
+
* ignoreWhitespace: true
|
|
157
|
+
* });
|
|
158
|
+
*/
|
|
159
|
+
async function diff(input1, input2, options = {}) {
|
|
160
|
+
const args = [input1, input2];
|
|
161
|
+
|
|
162
|
+
// Add format option
|
|
163
|
+
if (options.format) {
|
|
164
|
+
args.push('--format', options.format);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Add output format option
|
|
168
|
+
if (options.output) {
|
|
169
|
+
args.push('--output', options.output);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Add recursive option
|
|
173
|
+
if (options.recursive) {
|
|
174
|
+
args.push('--recursive');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Add path filter option
|
|
178
|
+
if (options.path) {
|
|
179
|
+
args.push('--path', options.path);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Add ignore keys regex option
|
|
183
|
+
if (options.ignoreKeysRegex) {
|
|
184
|
+
args.push('--ignore-keys-regex', options.ignoreKeysRegex);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Add epsilon option
|
|
188
|
+
if (options.epsilon !== undefined) {
|
|
189
|
+
args.push('--epsilon', options.epsilon.toString());
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Add array ID key option
|
|
193
|
+
if (options.arrayIdKey) {
|
|
194
|
+
args.push('--array-id-key', options.arrayIdKey);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Add optimize option
|
|
198
|
+
if (options.optimize) {
|
|
199
|
+
args.push('--optimize');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Add context option
|
|
203
|
+
if (options.context !== undefined) {
|
|
204
|
+
args.push('--context', options.context.toString());
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Add ignore whitespace option
|
|
208
|
+
if (options.ignoreWhitespace) {
|
|
209
|
+
args.push('--ignore-whitespace');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Add ignore case option
|
|
213
|
+
if (options.ignoreCase) {
|
|
214
|
+
args.push('--ignore-case');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Add quiet option
|
|
218
|
+
if (options.quiet) {
|
|
219
|
+
args.push('--quiet');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Add brief option
|
|
223
|
+
if (options.brief) {
|
|
224
|
+
args.push('--brief');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const { stdout, stderr } = await executeDiffx(args);
|
|
228
|
+
|
|
229
|
+
// If output format is JSON, parse the result
|
|
230
|
+
if (options.output === 'json') {
|
|
231
|
+
try {
|
|
232
|
+
const jsonData = JSON.parse(stdout);
|
|
233
|
+
return jsonData.map(item => {
|
|
234
|
+
if (item.Added) {
|
|
235
|
+
return {
|
|
236
|
+
type: 'Added',
|
|
237
|
+
path: item.Added[0],
|
|
238
|
+
newValue: item.Added[1]
|
|
239
|
+
};
|
|
240
|
+
} else if (item.Removed) {
|
|
241
|
+
return {
|
|
242
|
+
type: 'Removed',
|
|
243
|
+
path: item.Removed[0],
|
|
244
|
+
value: item.Removed[1]
|
|
245
|
+
};
|
|
246
|
+
} else if (item.Modified) {
|
|
247
|
+
return {
|
|
248
|
+
type: 'Modified',
|
|
249
|
+
path: item.Modified[0],
|
|
250
|
+
oldValue: item.Modified[1],
|
|
251
|
+
newValue: item.Modified[2]
|
|
252
|
+
};
|
|
253
|
+
} else if (item.TypeChanged) {
|
|
254
|
+
return {
|
|
255
|
+
type: 'TypeChanged',
|
|
256
|
+
path: item.TypeChanged[0],
|
|
257
|
+
oldValue: item.TypeChanged[1],
|
|
258
|
+
newValue: item.TypeChanged[2]
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
return item;
|
|
262
|
+
});
|
|
263
|
+
} catch (e) {
|
|
264
|
+
throw new DiffError(`Failed to parse JSON output: ${e.message}`, -1, '');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Return raw output for other formats
|
|
269
|
+
return stdout;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Compare two strings directly (writes to temporary files)
|
|
274
|
+
*
|
|
275
|
+
* @param {string} content1 - First content string
|
|
276
|
+
* @param {string} content2 - Second content string
|
|
277
|
+
* @param {Format} format - Content format
|
|
278
|
+
* @param {DiffOptions} [options={}] - Comparison options
|
|
279
|
+
* @returns {Promise<string|DiffResult[]>} String output for CLI format, or array of DiffResult for JSON format
|
|
280
|
+
*
|
|
281
|
+
* @example
|
|
282
|
+
* const json1 = '{"name": "Alice", "age": 30}';
|
|
283
|
+
* const json2 = '{"name": "Alice", "age": 31}';
|
|
284
|
+
* const result = await diffString(json1, json2, 'json', { output: 'json' });
|
|
285
|
+
* console.log(result);
|
|
286
|
+
*/
|
|
287
|
+
async function diffString(content1, content2, format, options = {}) {
|
|
288
|
+
// Ensure format is set
|
|
289
|
+
options.format = format;
|
|
290
|
+
|
|
291
|
+
// Create temporary files
|
|
292
|
+
const tmpDir = mkdtempSync(path.join(tmpdir(), 'diffx-'));
|
|
293
|
+
const tmpFile1 = path.join(tmpDir, `file1.${format}`);
|
|
294
|
+
const tmpFile2 = path.join(tmpDir, `file2.${format}`);
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
// Write content to temporary files
|
|
298
|
+
writeFileSync(tmpFile1, content1, 'utf8');
|
|
299
|
+
writeFileSync(tmpFile2, content2, 'utf8');
|
|
300
|
+
|
|
301
|
+
// Perform diff
|
|
302
|
+
return await diff(tmpFile1, tmpFile2, options);
|
|
303
|
+
} finally {
|
|
304
|
+
// Clean up temporary files
|
|
305
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Check if diffx command is available in the system
|
|
311
|
+
*
|
|
312
|
+
* @returns {Promise<boolean>} True if diffx is available, false otherwise
|
|
313
|
+
*
|
|
314
|
+
* @example
|
|
315
|
+
* if (!(await isDiffxAvailable())) {
|
|
316
|
+
* console.error('Please install diffx CLI tool');
|
|
317
|
+
* process.exit(1);
|
|
318
|
+
* }
|
|
319
|
+
*/
|
|
320
|
+
async function isDiffxAvailable() {
|
|
321
|
+
try {
|
|
322
|
+
await executeDiffx(['--version']);
|
|
323
|
+
return true;
|
|
324
|
+
} catch (err) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
module.exports = {
|
|
330
|
+
diff,
|
|
331
|
+
diffString,
|
|
332
|
+
isDiffxAvailable,
|
|
333
|
+
DiffError
|
|
334
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "diffx-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "A Node.js wrapper for the diffx CLI tool - semantic diffing of JSON, YAML, TOML, XML, INI, and CSV files. Focuses on structural meaning rather than formatting.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"diff",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"automation",
|
|
21
21
|
"data-analysis"
|
|
22
22
|
],
|
|
23
|
-
"main": "
|
|
23
|
+
"main": "lib.js",
|
|
24
24
|
"bin": {
|
|
25
25
|
"diffx": "./index.js"
|
|
26
26
|
},
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"files": [
|
|
38
38
|
"index.js",
|
|
39
|
+
"lib.js",
|
|
39
40
|
"scripts/download-binary.js",
|
|
40
41
|
"bin/",
|
|
41
42
|
"README.md",
|
package/test.js
CHANGED
|
@@ -40,7 +40,15 @@ const testData = {
|
|
|
40
40
|
json1: '{"name": "test-app", "version": "1.0.0", "debug": true}',
|
|
41
41
|
json2: '{"debug": false, "version": "1.1.0", "name": "test-app"}',
|
|
42
42
|
yaml1: 'name: test-app\nversion: "1.0.0"\ndebug: true\n',
|
|
43
|
-
yaml2: 'name: test-app\nversion: "1.1.0"\ndebug: false\n'
|
|
43
|
+
yaml2: 'name: test-app\nversion: "1.1.0"\ndebug: false\n',
|
|
44
|
+
|
|
45
|
+
// Test data for new options
|
|
46
|
+
caseTest1: '{"status": "Active", "level": "Info"}',
|
|
47
|
+
caseTest2: '{"status": "ACTIVE", "level": "INFO"}',
|
|
48
|
+
whitespaceTest1: '{"text": "Hello World", "message": "Test\\tValue"}',
|
|
49
|
+
whitespaceTest2: '{"text": "Hello World", "message": "Test Value"}',
|
|
50
|
+
contextTest1: '{"host": "localhost", "port": 5432, "name": "myapp"}',
|
|
51
|
+
contextTest2: '{"host": "localhost", "port": 5433, "name": "myapp"}'
|
|
44
52
|
};
|
|
45
53
|
|
|
46
54
|
// Create temporary test directory
|
|
@@ -212,6 +220,65 @@ async function runTests() {
|
|
|
212
220
|
throw new Error('Error handling failed');
|
|
213
221
|
}
|
|
214
222
|
|
|
223
|
+
// Test 8: API functionality with new options
|
|
224
|
+
info('Test 8: Testing API functionality with new options...');
|
|
225
|
+
|
|
226
|
+
// Test ignore case option
|
|
227
|
+
try {
|
|
228
|
+
const { diff, diffString } = require('./lib.js');
|
|
229
|
+
|
|
230
|
+
// Test ignore case
|
|
231
|
+
const caseResult = await diffString(testData.caseTest1, testData.caseTest2, 'json', {
|
|
232
|
+
ignoreCase: true,
|
|
233
|
+
output: 'json'
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
if (Array.isArray(caseResult) && caseResult.length === 0) {
|
|
237
|
+
success('API ignore-case option works correctly');
|
|
238
|
+
} else {
|
|
239
|
+
info('API ignore-case test completed (may show differences)');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Test ignore whitespace
|
|
243
|
+
const whitespaceResult = await diffString(testData.whitespaceTest1, testData.whitespaceTest2, 'json', {
|
|
244
|
+
ignoreWhitespace: true,
|
|
245
|
+
output: 'json'
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
if (Array.isArray(whitespaceResult) && whitespaceResult.length === 0) {
|
|
249
|
+
success('API ignore-whitespace option works correctly');
|
|
250
|
+
} else {
|
|
251
|
+
info('API ignore-whitespace test completed (may show differences)');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Test quiet option
|
|
255
|
+
const quietResult = await diffString(testData.json1, testData.json2, 'json', {
|
|
256
|
+
quiet: true
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (quietResult === '') {
|
|
260
|
+
success('API quiet option works correctly');
|
|
261
|
+
} else {
|
|
262
|
+
info('API quiet test completed');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Test brief option
|
|
266
|
+
const briefResult = await diffString(testData.json1, testData.json2, 'json', {
|
|
267
|
+
brief: true
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (typeof briefResult === 'string') {
|
|
271
|
+
success('API brief option works correctly');
|
|
272
|
+
} else {
|
|
273
|
+
info('API brief test completed');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
success('API tests completed successfully');
|
|
277
|
+
|
|
278
|
+
} catch (apiErr) {
|
|
279
|
+
info(`API test completed with info: ${apiErr.message}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
215
282
|
success('All tests passed!');
|
|
216
283
|
info('diffx-js package is working correctly');
|
|
217
284
|
|