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.
Files changed (3) hide show
  1. package/lib.js +334 -0
  2. package/package.json +3 -2
  3. 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.2",
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": "index.js",
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