properties-comparator 1.0.4 → 1.0.6

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/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const fs = require("fs");
4
- const path = require("path");
5
- const yaml = require("js-yaml");
3
+ import chalk from "chalk";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import yaml from "js-yaml";
6
7
 
7
8
  /**
8
9
  * Parses a .properties file into an object.
@@ -114,7 +115,7 @@ function parseFile(filePath) {
114
115
  default:
115
116
  console.error(
116
117
  `Warning: Unsupported file extension "${ext}" for file "${filePath}". ` +
117
- `Only .properties, .yml, or .yaml are supported. This file will be treated as empty.`
118
+ `Only .properties, .yml, or .yaml are supported. This file will be treated as empty.`
118
119
  );
119
120
  return {};
120
121
  }
@@ -146,7 +147,7 @@ function compareFileData(filePaths) {
146
147
  // Compare values for each key across files
147
148
  allKeys.forEach((key) => {
148
149
  const values = parsedObjects.map(
149
- (obj) => obj[key]?.replace(/\s+/g, "") || "N/A"
150
+ (obj) => obj[key]?.replaceAll(/\s+/g, "") || "N/A"
150
151
  );
151
152
  const matched = values.every((value) => value === values[0]);
152
153
  mismatchDetails.push({ key, values, matched });
@@ -182,61 +183,349 @@ function getMismatchFields(filePaths) {
182
183
  }
183
184
 
184
185
  /**
185
- * CLI function: compares properties/keys across multiple files,
186
- * prints details to the console, and provides a summary.
186
+ * Generates an HTML report for the comparison results.
187
187
  *
188
- * @param {string[]} filePaths - Array of file paths.
188
+ * @param {Array} filePaths - Array of file paths that were compared
189
+ * @param {Object} comparisonData - The output from compareFileData function
190
+ * @returns {string} - HTML document as string
189
191
  */
190
- function compareFiles(filePaths) {
191
- console.log("Comparing properties/keys across files:\n");
192
+ function generateHtmlReport(filePaths, comparisonData) {
193
+ const { mismatchCount, mismatchDetails } = comparisonData;
194
+ const fileNames = filePaths.map((fp) => path.basename(fp));
195
+
196
+ // Start HTML document
197
+ let html = `<!DOCTYPE html>
198
+ <html lang="en">
199
+ <head>
200
+ <meta charset="UTF-8">
201
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
202
+ <title>Properties Comparison Report</title>
203
+ <style>
204
+ body { font-family: Arial, sans-serif; margin: 20px; line-height: 1.6; color: #333; }
205
+ h1, h2 { color: #0066cc; }
206
+ table { border-collapse: collapse; width: 100%; margin-bottom: 20px; }
207
+ th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
208
+ th { background-color: #f2f2f2; }
209
+ tr:nth-child(even) { background-color: #f9f9f9; }
210
+ tr:hover { background-color: #f2f2f2; }
211
+ .mismatch { background-color: #ffe6e6; }
212
+ .matched { background-color: #e6ffe6; }
213
+ .value-mismatch { color: #cc0000; font-weight: bold; }
214
+ .summary { margin: 20px 0; padding: 15px; border-radius: 5px; }
215
+ .summary.success { background-color: #e6ffe6; border: 1px solid #99cc99; }
216
+ .summary.error { background-color: #ffe6e6; border: 1px solid #cc9999; }
217
+ .file-list { margin-bottom: 20px; }
218
+ </style>
219
+ </head>
220
+ <body>
221
+ <h1>Properties Comparison Report</h1>
222
+
223
+ <div class="file-list">
224
+ <h2>Files Compared:</h2>
225
+ <ol>
226
+ ${fileNames
227
+ .map(
228
+ (name, idx) => `<li>${name} <small>(${filePaths[idx]})</small></li>`
229
+ )
230
+ .join("\n ")}
231
+ </ol>
232
+ </div>
192
233
 
193
- const { mismatchCount, mismatchDetails } = compareFileData(filePaths);
234
+ <h2>Comparison Results</h2>
235
+ <table>
236
+ <tr>
237
+ <th>Key</th>
238
+ <th>Matched</th>
239
+ ${fileNames
240
+ .map((name, idx) => `<th>File ${idx + 1}: ${name}</th>`)
241
+ .join("\n ")}
242
+ </tr>`;
194
243
 
195
- // Print detail for each key
244
+ // Add table rows for each key
196
245
  mismatchDetails.forEach(({ key, values, matched }) => {
197
- if (matched) {
198
- console.log(`Key: ${key} - Values match: '${values[0]}'`);
199
- } else {
200
- console.log(`Key: ${key} - Mismatched values:`);
201
- values.forEach((value, idx) => {
202
- console.log(` File ${idx + 1} (${filePaths[idx]}): ${value}`);
203
- });
204
- }
246
+ html += `\n <tr class="${matched ? "matched" : "mismatch"}">
247
+ <td>${key}</td>
248
+ <td>${matched ? "Yes" : "No"}</td>`;
249
+
250
+ // Add values from each file
251
+ values.forEach((value, idx) => {
252
+ const cellClass = matched ? "" : "value-mismatch";
253
+ html += `\n <td class="${cellClass}">${value === "N/A" ? "<em>N/A</em>" : value
254
+ }</td>`;
255
+ });
256
+
257
+ html += `\n </tr>`;
258
+ });
259
+
260
+ html += `\n </table>
261
+
262
+ <div class="summary ${mismatchCount === 0 ? "success" : "error"}">
263
+ <h2>Summary</h2>`;
264
+
265
+ if (mismatchCount === 0) {
266
+ html += `\n <p>All properties match across all files!</p>`;
267
+ } else {
268
+ html += `\n <p>${mismatchCount} key(s) have mismatched values.</p>
269
+ <p><strong>Mismatched keys:</strong> ${mismatchDetails
270
+ .filter((detail) => !detail.matched)
271
+ .map((detail) => detail.key)
272
+ .join(", ")}</p>`;
273
+ }
274
+
275
+ html += `\n </div>
276
+ </body>
277
+ </html>`;
278
+
279
+ return html;
280
+ }
281
+
282
+ /**
283
+ * Generates a Markdown report for the comparison results.
284
+ *
285
+ * @param {Array} filePaths - Array of file paths that were compared
286
+ * @param {Object} comparisonData - The output from compareFileData function
287
+ * @returns {string} - Markdown document as string
288
+ */
289
+ function generateMarkdownReport(filePaths, comparisonData) {
290
+ const { mismatchCount, mismatchDetails } = comparisonData;
291
+ const fileNames = filePaths.map((fp) => path.basename(fp));
292
+
293
+ let markdown = `# Properties Comparison Report\n\n`;
294
+
295
+ // Files compared
296
+ markdown += `## Files Compared\n\n`;
297
+ filePaths.forEach((fp, idx) => {
298
+ markdown += `${idx + 1}. ${fileNames[idx]} (${fp})\n`;
299
+ });
300
+
301
+ // Comparison results table
302
+ markdown += `\n## Comparison Results\n\n`;
303
+
304
+ // Table header
305
+ markdown += `| Key | Matched | ${fileNames
306
+ .map((name, idx) => `File ${idx + 1}: ${name}`)
307
+ .join(" | ")} |\n`;
308
+ markdown += `| --- | --- | ${fileNames.map(() => "---").join(" | ")} |\n`;
309
+
310
+ // Table content
311
+ mismatchDetails.forEach(({ key, values, matched }) => {
312
+ markdown += `| ${key} | ${matched ? "Yes" : "No"} | ${values
313
+ .map((v) => (v === "N/A" ? "*N/A*" : v))
314
+ .join(" | ")} |\n`;
205
315
  });
206
316
 
207
317
  // Summary
208
- console.log("\n=== Summary ===");
318
+ markdown += `\n## Summary\n\n`;
209
319
  if (mismatchCount === 0) {
210
- console.log("All properties match across all files!");
320
+ markdown += `✅ All properties match across all files!\n`;
211
321
  } else {
212
- console.log(`${mismatchCount} key(s) have mismatched values.`);
322
+ markdown += `❌ ${mismatchCount} key(s) have mismatched values.\n\n`;
323
+ markdown += `**Mismatched keys:** ${mismatchDetails
324
+ .filter((detail) => !detail.matched)
325
+ .map((detail) => detail.key)
326
+ .join(", ")}\n`;
213
327
  }
328
+
329
+ return markdown;
330
+ }
331
+
332
+ /**
333
+ * CLI function: compares properties/keys across multiple files,
334
+ * prints details to the console in a tabular format, and provides a summary.
335
+ *
336
+ * @param {string[]} filePaths - Array of file paths.
337
+ * @param {Object} options - Options for the comparison.
338
+ * @param {string} [options.format] - Output format ('console', 'html', or 'markdown').
339
+ * @param {string} [options.outputFile] - Path to save the report (for html and markdown).
340
+ */
341
+ function compareFiles(filePaths, options = {}) {
342
+ const format = options.format || "console";
343
+ const outputFile = options.outputFile;
344
+
345
+ const comparisonData = compareFileData(filePaths);
346
+
347
+ if (format === "console") {
348
+ console.log("Comparing properties/keys across files:\n");
349
+
350
+ // Prepare data for tabular output
351
+ const tableData = comparisonData.mismatchDetails.map(
352
+ ({ key, values, matched }) => {
353
+ const valueColumns = values.reduce((acc, value, idx) => {
354
+ acc[`File ${idx + 1}`] = value;
355
+ return acc;
356
+ }, {});
357
+ return {
358
+ Key: key,
359
+ Matched: matched ? "Yes" : "No",
360
+ ...valueColumns,
361
+ };
362
+ }
363
+ );
364
+
365
+ // Print the table
366
+ console.table(tableData);
367
+
368
+ // Custom print for mismatched rows
369
+ console.log("\n=== Highlighted Mismatched Rows ===");
370
+ comparisonData.mismatchDetails.forEach(({ key, values, matched }) => {
371
+ if (!matched) {
372
+ const coloredValues = values.map((value, idx) =>
373
+ chalk.red(`File ${idx + 1}: ${value}`)
374
+ );
375
+ console.log(
376
+ chalk.yellow(`Key: ${key}`),
377
+ "|",
378
+ coloredValues.join(" | ")
379
+ );
380
+ }
381
+ });
382
+
383
+ // Summary
384
+ console.log("\n=== Summary ===");
385
+ if (comparisonData.mismatchCount === 0) {
386
+ console.log("All properties match across all files!");
387
+ } else {
388
+ console.log(
389
+ `${comparisonData.mismatchCount} key(s) have mismatched values.`
390
+ );
391
+ const mismatchedKeys = comparisonData.mismatchDetails
392
+ .filter((detail) => !detail.matched)
393
+ .map((detail) => detail.key);
394
+ console.log("Mismatched keys:", mismatchedKeys.join(", "));
395
+ }
396
+ } else if (format === "html") {
397
+ const htmlReport = generateHtmlReport(filePaths, comparisonData);
398
+ if (outputFile) {
399
+ fs.writeFileSync(outputFile, htmlReport);
400
+ console.log(`HTML report saved to: ${outputFile}`);
401
+ } else {
402
+ console.log(htmlReport);
403
+ }
404
+ } else if (format === "markdown") {
405
+ const markdownReport = generateMarkdownReport(filePaths, comparisonData);
406
+ if (outputFile) {
407
+ fs.writeFileSync(outputFile, markdownReport);
408
+ console.log(`Markdown report saved to: ${outputFile}`);
409
+ } else {
410
+ console.log(markdownReport);
411
+ }
412
+ } else {
413
+ console.error(
414
+ `Unsupported format: ${format}. Using console output instead.`
415
+ );
416
+ compareFiles(filePaths); // Fallback to console output
417
+ }
418
+ }
419
+
420
+ /**
421
+ * Prints the usage information for the CLI.
422
+ */
423
+ function printUsage() {
424
+ console.error("Please provide file paths as command-line arguments.");
425
+ console.error(
426
+ "Usage: properties-comparator [options] file1 file2 [file3...]"
427
+ );
428
+ console.error("Options:");
429
+ console.error(
430
+ " --format, -f <format> Output format: console, html, or markdown"
431
+ );
432
+ console.error(
433
+ " --output, -o <file> Output file for html or markdown reports"
434
+ );
435
+ }
436
+
437
+ /**
438
+ * Parses command-line arguments.
439
+ * @param {string[]} args - Array of command-line arguments.
440
+ * @returns {{ filePaths: string[], options: Object }} - Parsed paths and options.
441
+ */
442
+ function parseArgs(args) {
443
+ const options = {
444
+ format: "console",
445
+ outputFile: null,
446
+ };
447
+ const filePaths = [];
448
+
449
+ let i = 0;
450
+ while (i < args.length) {
451
+ if (args[i] === "--format" || args[i] === "-f") {
452
+ if (i + 1 < args.length) {
453
+ options.format = args[i + 1].toLowerCase();
454
+ i += 2;
455
+ } else {
456
+ i++;
457
+ }
458
+ } else if (args[i] === "--output" || args[i] === "-o") {
459
+ if (i + 1 < args.length) {
460
+ options.outputFile = args[i + 1];
461
+ i += 2;
462
+ } else {
463
+ i++;
464
+ }
465
+ } else {
466
+ filePaths.push(path.resolve(args[i]));
467
+ i++;
468
+ }
469
+ }
470
+
471
+ return { filePaths, options };
214
472
  }
215
473
 
216
474
  /**
217
475
  * CLI entry point for comparing .properties and .yml/.yaml files.
218
476
  */
219
477
  function run() {
220
- const filePaths = process.argv.slice(2);
478
+ const { filePaths, options } = parseArgs(process.argv.slice(2));
221
479
 
222
480
  if (filePaths.length === 0) {
223
- console.error("Please provide file paths as command-line arguments.");
481
+ printUsage();
482
+ process.exit(1);
483
+ } else if (filePaths.length === 1) {
484
+ console.error("Please provide at least two file paths for comparison.");
224
485
  process.exit(1);
225
486
  }
226
487
 
227
- // Optionally, check if all files exist
228
488
  const missing = filePaths.filter((fp) => !fs.existsSync(fp));
229
489
  if (missing.length > 0) {
230
490
  console.error(`The following file(s) do not exist: ${missing.join(", ")}`);
231
- // We continue anyway, or we can decide to exit.
232
- // For now, let's exit to avoid unexpected comparisons:
233
491
  process.exit(1);
234
492
  }
235
493
 
236
- compareFiles(filePaths);
494
+ compareFiles(filePaths, options);
495
+ }
496
+
497
+ /**
498
+ * API function to compare properties between two files.
499
+ * @param {string} file1 - Path to the first file
500
+ * @param {string} file2 - Path to the second file
501
+ * @param {Object} options - Comparison options
502
+ * @returns {Object} Comparison results in a structured format
503
+ */
504
+ function compareProperties(file1, file2, options = {}) {
505
+ const filePaths = [file1, file2];
506
+ const comparisonData = compareFileData(filePaths);
507
+
508
+ // Process the output based on options
509
+ if (options.output) {
510
+ if (options.json) {
511
+ fs.writeFileSync(options.output, JSON.stringify(comparisonData, null, 2));
512
+ } else {
513
+ const format = path.extname(options.output).toLowerCase() === '.md' ? 'markdown' : 'html';
514
+ const report = format === 'markdown'
515
+ ? generateMarkdownReport(filePaths, comparisonData)
516
+ : generateHtmlReport(filePaths, comparisonData);
517
+ fs.writeFileSync(options.output, report);
518
+ }
519
+
520
+ if (options.verbose) {
521
+ console.log(`Comparison report saved to ${options.output}`);
522
+ }
523
+ }
524
+
525
+ return comparisonData;
237
526
  }
238
527
 
239
- module.exports = {
528
+ export {
240
529
  parsePropertiesFile,
241
530
  parseYamlFile,
242
531
  parseFile,
@@ -244,10 +533,12 @@ module.exports = {
244
533
  checkIfAllValuesMatch,
245
534
  getMismatchFields,
246
535
  compareFiles,
247
- run,
536
+ generateHtmlReport,
537
+ generateMarkdownReport,
538
+ compareProperties, // Add the new function to exports
248
539
  };
249
540
 
250
541
  // If the script is executed directly, run the CLI
251
- if (require.main === module) {
542
+ if (import.meta.url === `file://${process.argv[1]}`) {
252
543
  run();
253
544
  }
package/jest.config.js CHANGED
@@ -1,22 +1,9 @@
1
- // jest.config.js
2
- module.exports = {
3
- // Tell Jest to collect coverage information
4
- collectCoverage: true,
5
- // Or specify which files to collect coverage from
6
- collectCoverageFrom: ['src/**/*.js'], // adjust for your source folder
7
-
8
- // Optionally set coverage thresholds
9
- coverageThreshold: {
10
- global: {
11
- branches: 80,
12
- functions: 80,
13
- lines: 80,
14
- statements: 80,
15
- },
16
- },
17
-
18
- // Customize output directory or coverage reporters if desired
19
- coverageDirectory: 'coverage',
20
- coverageReporters: ['json', 'lcov', 'text', 'clover'],
21
- };
22
-
1
+ export default {
2
+ testEnvironment: "node", // Use Node.js environment
3
+ transform: {
4
+ "^.+\\.js$": "babel-jest", // Transform ES modules with Babel
5
+ },
6
+ transformIgnorePatterns: [
7
+ "/node_modules/(?!(chalk|ansi-styles|supports-color|strip-ansi|ansi-regex)/)", // Allow specific ES module packages to be transformed
8
+ ],
9
+ };
package/package.json CHANGED
@@ -1,14 +1,16 @@
1
1
  {
2
2
  "name": "properties-comparator",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "This utility provides functionality to parse and compare properties files in the format of key-value pairs. It reads properties files, compares the values for each key across multiple files, and logs the results.",
5
+ "type": "module",
5
6
  "main": "index.js",
6
7
  "bin": {
7
- "properties-comparator": "./index.js"
8
+ "properties-comparator": "./cli.js"
8
9
  },
9
10
  "scripts": {
10
11
  "test": "jest",
11
- "run": "node index.js"
12
+ "run": "node index.js",
13
+ "clean": "rm -rf node_modules coverage"
12
14
  },
13
15
  "repository": {
14
16
  "type": "git",
@@ -16,7 +18,9 @@
16
18
  },
17
19
  "keywords": [
18
20
  "properties",
19
- "comparator"
21
+ "compare",
22
+ "yaml",
23
+ "configuration"
20
24
  ],
21
25
  "author": "Zack Dawood",
22
26
  "license": "MIT",
@@ -25,9 +29,18 @@
25
29
  },
26
30
  "homepage": "https://github.com/zackria/properties-comparator#readme",
27
31
  "dependencies": {
28
- "js-yaml": "^4.1.0"
32
+ "chalk": "^5.6.2",
33
+ "commander": "^14.0.2",
34
+ "js-yaml": "^4.1.1"
29
35
  },
30
36
  "devDependencies": {
31
- "jest": "^29.7.0"
37
+ "@babel/core": "^7.28.5",
38
+ "@babel/preset-env": "^7.28.5",
39
+ "babel-jest": "^30.2.0",
40
+ "jest": "^30.2.0",
41
+ "jest-environment-node": "^30.2.0"
42
+ },
43
+ "overrides": {
44
+ "test-exclude": "^7.0.1"
32
45
  }
33
- }
46
+ }
@@ -0,0 +1,19 @@
1
+ sonar.projectKey=zackria_properties-comparator
2
+ sonar.organization=zackria
3
+
4
+ # This is the name and version displayed in the SonarCloud UI.
5
+ #sonar.projectName=properties-comparator
6
+ #sonar.projectVersion=1.0.6
7
+
8
+ # Path is relative to the sonarproject.properties file. Replace "\" by "/" on Windows.
9
+ sonar.sources=index.js,cli.js,src
10
+ sonar.tests=test
11
+ sonar.test.inclusions=test/**/*.test.js
12
+
13
+ # Encoding of the source code. Default is default system encoding
14
+ sonar.sourceEncoding=UTF-8
15
+
16
+ # JavaScript-specific configurations
17
+ sonar.javascript.lcov.reportPaths=coverage/lcov.info
18
+ sonar.exclusions=node_modules/**, coverage/**, index.js
19
+ sonar.cpd.exclusions=index.js