Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 105 additions & 26 deletions packages/jsondiffpatch/bin/jsondiffpatch.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,55 +3,134 @@
import { readFileSync } from "node:fs";
import * as consoleFormatter from "../lib/formatters/console.js";
import * as jsonpatchFormatter from "../lib/formatters/jsonpatch.js";
import * as jsondiffpatch from "../lib/with-text-diffs.js";
import { create } from "../lib/with-text-diffs.js";

const allowedFlags = ["--help", "--format=json", "--format=jsonpatch"];
const allowedFlags = [
"--help",
"--format",
"--omit-removed-values",
"--no-moves",
"--no-text-diff",
"--object-keys",
];

const args = process.argv.slice(2);
const flags = [];
const flags = {};
const files = [];
for (const arg of args) {
if (arg.startsWith("--")) {
if (allowedFlags.indexOf(arg) === -1) {
console.error(`unrecognized option: ${arg}`);
const argParts = arg.split("=");
if (allowedFlags.indexOf(argParts[0]) === -1) {
console.error(`unrecognized option: ${argParts[0]}`);
process.exit(2);
}
flags.push(arg);
flags[argParts[0]] = argParts[1] ?? true;
} else {
files.push(arg);
}
}

const usage =
"usage: jsondiffpatch left.json right.json" +
"\n" +
"\n note: http and https URLs are also supported\n";
const usage = () => {
return `usage: jsondiffpatch left.json right.json
note: http and https URLs are also supported

flags:
--format=console (default) print a readable colorized diff
--format=json output the pure JSON diff
--format=json-compact pure JSON diff, no indentation
--format=jsonpatch output JSONPatch (RFC 6902)

--omit-removed-values omits removed values from the diff
--no-moves disable array moves detection
--no-text-diff disable text diffs
--object-keys=... (defaults to: id,key) optional comma-separated properties to match 2 objects between array versions (see objectHash)

example:`;
};

function createInstance() {
const format =
typeof flags["--format"] === "string" ? flags["--format"] : "console";
const objectKeys = (flags["--object-keys="] ?? "id,key")
.split(",")
.map((key) => key.trim());

const jsondiffpatch = create({
objectHash: (obj, index) => {
if (obj && typeof obj === "object") {
for (const key of objectKeys) {
if (key in obj) {
return obj[key];
}
}
}
return index;
},
arrays: {
detectMove: !flags["--no-moves"],
},
omitRemovedValues: !!flags["--omit-removed-values"],
textDiff: {
...(format === "jsonpatch" || !!flags["--no-text-diff"]
? {
// text diff not supported by jsonpatch
minLength: Number.MAX_VALUE,
}
: {}),
},
});
return jsondiffpatch;
}

function printDiff(delta) {
if (flags["--format"] === "json") {
console.log(JSON.stringify(delta, null, 2));
} else if (flags["--format"] === "json-compact") {
console.log(JSON.stringify(delta));
} else if (flags["--format"] === "jsonpatch") {
jsonpatchFormatter.log(delta);
} else {
consoleFormatter.log(delta);
}
}

function getJson(path) {
if (/^https?:\/\//i.test(path)) {
// an absolute URL, fetch it
return fetch(path).then((response) => response.json());
}
return JSON.parse(readFileSync(path));
}

const jsondiffpatch = createInstance();

if (files.length !== 2 || flags.includes("--help")) {
console.log(usage);
console.log(usage());
const delta = jsondiffpatch.diff(
{
property: "before",
list: [{ id: 1 }, { id: 2 }, { id: 3, name: "item removed" }],
longText:
"when a text is very 🦕 long, diff-match-patch is used to create a text diff that only captures the changes, comparing each characther",
},
{
property: "after",
newProperty: "added",
list: [{ id: 2 }, { id: 1 }, { id: 4, name: "item added" }],
longText:
"when a text a bit long, diff-match-patch creates a text diff that captures the changes, comparing each characther",
},
);
printDiff(delta);
} else {
Promise.all([files[0], files[1]].map(getJson)).then(([left, right]) => {
const delta = jsondiffpatch.diff(left, right);
if (delta === undefined) {
process.exit(0);
} else {
if (flags.includes("--format=json")) {
console.log(JSON.stringify(delta, null, 2));
} else if (flags.includes("--format=jsonpatch")) {
jsonpatchFormatter.log(delta);
} else {
consoleFormatter.log(delta);
}
printDiff(delta);
// exit code 1 to be consistent with GNU diff
process.exit(1);
}
});
}

function getJson(path) {
if (/^https?:\/\//i.test(path)) {
// an absolute URL, fetch it
return fetch(path).then((response) => response.json());
}
return JSON.parse(readFileSync(path));
}
27 changes: 18 additions & 9 deletions packages/jsondiffpatch/src/formatters/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ abstract class BaseFormatter<
for (let index = 0; index < keys.length; index++) {
const key = keys[index];
if (key === undefined) continue;
const isLast = index === length - 1;
const isLast = index === keys.length - 1;
fn(
// for object diff, the delta key and left key are the same
key,
Expand Down Expand Up @@ -310,8 +310,6 @@ abstract class BaseFormatter<
rightIndex < rightLength ||
`${rightIndex}` in arrayDelta
) {
const isLast =
leftIndex === leftLength - 1 || rightIndex === rightLength - 1;
let hasDelta = false;

const leftIndexKey = `_${leftIndex}` as const;
Expand All @@ -333,7 +331,12 @@ abstract class BaseFormatter<
value: leftArray ? leftArray[movedFromIndex] : undefined,
}
: undefined,
isLast && !(rightIndexKey in arrayDelta),

// is this the last key in this delta?
leftIndex === leftLength - 1 &&
rightIndex === rightLength - 1 &&
!(`${rightIndex + 1}` in arrayDelta) &&
!(rightIndexKey in arrayDelta),
);

if (Array.isArray(itemDelta)) {
Expand All @@ -357,6 +360,7 @@ abstract class BaseFormatter<
// something happened to the right item at this position
hasDelta = true;
const itemDelta = arrayDelta[rightIndexKey];
const isItemAdded = Array.isArray(itemDelta) && itemDelta.length === 1;
fn(
rightIndexKey,
movedFromIndex ?? leftIndex,
Expand All @@ -366,10 +370,14 @@ abstract class BaseFormatter<
value: leftArray ? leftArray[movedFromIndex] : undefined,
}
: undefined,
isLast,
// is this the last key in this delta?
leftIndex === leftLength - 1 &&
rightIndex === rightLength - 1 + (isItemAdded ? 1 : 0) &&
!(`_${leftIndex + 1}` in arrayDelta) &&
!(`${rightIndex + 1}` in arrayDelta),
);

if (Array.isArray(itemDelta) && itemDelta.length === 1) {
if (isItemAdded) {
// added
rightLength++;
rightIndex++;
Expand Down Expand Up @@ -398,7 +406,10 @@ abstract class BaseFormatter<
value: leftArray ? leftArray[movedFromIndex] : undefined,
}
: undefined,
isLast,
// is this the last key in this delta?
leftIndex === leftLength - 1 &&
rightIndex === rightLength - 1 &&
!(`${rightIndex + 1}` in arrayDelta),
);
}

Expand Down Expand Up @@ -446,9 +457,7 @@ abstract class BaseFormatter<
parseTextDiff(value: string) {
const output = [];
const lines = value.split("\n@@ ");
// for (let i = 0, l = lines.length; i < l; i++) {
for (const line of lines) {
//const line = lines[i];
const lineOutput: {
pieces: LineOutputPiece[];
location?: LineOutputLocation;
Expand Down
2 changes: 1 addition & 1 deletion packages/jsondiffpatch/src/formatters/console.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ class ConsoleFormatter extends BaseFormatter<ConsoleFormatterContext> {
const pieces = line.pieces;
for (const piece of pieces) {
context.pushColor(this.brushes[piece.type]);
context.out(piece.text);
context.out(decodeURI(piece.text));
context.popColor();
}
if (i < lines.length - 1) {
Expand Down