Package Exports
- @donedeal0/superdiff
- @donedeal0/superdiff/dist/index.js
- @donedeal0/superdiff/dist/index.mjs
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@donedeal0/superdiff) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
WHAT IS IT?
This library compares two arrays or objects and returns a full diff of their differences.
WHY YOU SHOULD USE THIS LIBRARY
Most existing solutions return a confusing diff format that often requires extra parsing. They are also limited to object comparison.
Superdiff provides a complete and readable diff for both arrays and objects. Plus, it supports stream and file inputs for handling large datasets efficiently, is battle-tested, has zero dependencies, and is super fast.
Import. Enjoy. 👍
DONORS
I am grateful to the generous donors of Superdiff!
FEATURES
Superdiff exports 5 functions:
// Returns a complete diff of two objects
getObjectDiff(prevObject, nextObject)
// Returns a complete diff of two arrays
getListDiff(prevList, nextList)
// Streams the diff of two object lists, ideal for large lists and maximum performance
streamListDiff(prevList, nextList, referenceProperty)
// Checks whether two values are equal
isEqual(dataA, dataB)
// Checks whether a value is an object
isObject(data)getObjectDiff()
import { getObjectDiff } from "@donedeal0/superdiff";Compares two objects and returns a diff for each value and its possible subvalues. Supports deeply nested objects of any value type.
FORMAT
Input
prevData: Record<string, unknown>;
nextData: Record<string, unknown>;
options?: {
ignoreArrayOrder?: boolean, // false by default,
showOnly?: {
statuses: ("added" | "deleted" | "updated" | "equal")[], // [] by default
granularity?: "basic" | "deep" // "basic" by default
}
}prevData: the original object.nextData: the new object.optionsignoreArrayOrder: if set totrue,["hello", "world"]and["world", "hello"]will be treated asequal, because the two arrays contain the same values, just in a different order.showOnly: returns only the values whose status you are interested in. It takes two parameters:statuses: status you want to see in the output (e.g.["added", "equal"])granularity:basicreturns only the main properties whose status matches your query.deepcan return main properties if some of their subproperties' status match your request. The subproperties are filtered accordingly.
Output
type ObjectDiff = {
type: "object";
status: "added" | "deleted" | "equal" | "updated";
diff: Diff[];
};
/** recursive diff in case of subproperties */
type Diff = {
property: string;
previousValue: unknown;
currentValue: unknown;
status: "added" | "deleted" | "equal" | "updated";
diff?: Diff[];
};USAGE
Input
getObjectDiff(
{
id: 54,
user: {
name: "joe",
- member: true,
- hobbies: ["golf", "football"],
age: 66,
},
},
{
id: 54,
user: {
name: "joe",
+ member: false,
+ hobbies: ["golf", "chess"],
age: 66,
},
}
);Output
{
type: "object",
+ status: "updated",
diff: [
{
property: "id",
previousValue: 54,
currentValue: 54,
status: "equal",
},
{
property: "user",
previousValue: {
name: "joe",
member: true,
hobbies: ["golf", "football"],
age: 66,
},
currentValue: {
name: "joe",
member: false,
hobbies: ["golf", "chess"],
age: 66,
},
+ status: "updated",
diff: [
{
property: "name",
previousValue: "joe",
currentValue: "joe",
status: "equal",
},
+ {
+ property: "member",
+ previousValue: true,
+ currentValue: false,
+ status: "updated",
+ },
+ {
+ property: "hobbies",
+ previousValue: ["golf", "football"],
+ currentValue: ["golf", "chess"],
+ status: "updated",
+ },
{
property: "age",
previousValue: 66,
currentValue: 66,
status: "equal",
},
],
},
],
}getListDiff()
import { getListDiff } from "@donedeal0/superdiff";Compares two arrays and returns a diff for each entry. Supports duplicate values, primitive values and objects.
FORMAT
Input
prevList: T[];
nextList: T[];
options?: {
showOnly?: ("added" | "deleted" | "moved" | "updated" | "equal")[], // [] by default
referenceProperty?: string, // "" by default
ignoreArrayOrder?: boolean, // false by default,
considerMoveAsUpdate?: boolean // false by default
}prevList: the original list.nextList: the new list.optionsshowOnlygives you the option to return only the values whose status you are interested in (e.g.["added", "equal"]).referencePropertywill consider an object to beupdatedrather thanaddedordeletedif one of its properties remains stable, such as itsid. This option has no effect on other datatypes.ignoreArrayOrder: if set totrue,["hello", "world"]and["world", "hello"]will be treated asequal, because the two arrays contain the same values, just in a different order.considerMoveAsUpdate: if set totrueamovedvalue will be considered asupdated.
Output
type ListDiff = {
type: "list";
status: "added" | "deleted" | "equal" | "moved" | "updated";
diff: {
value: unknown;
prevIndex: number | null;
newIndex: number | null;
indexDiff: number | null;
status: "added" | "deleted" | "equal" | "moved" | "updated";
}[];
};USAGE
Input
getListDiff(
- ["mbappe", "mendes", "verratti", "ruiz"],
+ ["mbappe", "messi", "ruiz"]
);Output
{
type: "list",
+ status: "updated",
diff: [
{
value: "mbappe",
prevIndex: 0,
newIndex: 0,
indexDiff: 0,
status: "equal",
},
- {
- value: "mendes",
- prevIndex: 1,
- newIndex: null,
- indexDiff: null,
- status: "deleted",
- },
- {
- value: "verratti",
- prevIndex: 2,
- newIndex: null,
- indexDiff: null,
- status: "deleted",
- },
+ {
+ value: "messi",
+ prevIndex: null,
+ newIndex: 1,
+ indexDiff: null,
+ status: "added",
+ },
+ {
+ value: "ruiz",
+ prevIndex: 3,
+ newIndex: 2,
+ indexDiff: -1,
+ status: "moved",
},
],
}streamListDiff()
// If you are in a server environment
import { streamListDiff } from "@donedeal0/superdiff/server";
// If you are in a browser environment
import { streamListDiff } from "@donedeal0/superdiff/client";Streams the diff of two object lists, ideal for large lists and maximum performance.
FORMAT
Input
Server
In a server environment,
Readablerefers to Node.js streams, andFilePathrefers to the path of a file (e.g.,./list.json). Examples are provided in the #usage section below.
prevList: Readable | FilePath | Record<string, unknown>[],
nextList: Readable | FilePath | Record<string, unknown>[],
referenceProperty: keyof Record<string, unknown>,
options: {
showOnly?: ("added" | "deleted" | "moved" | "updated" | "equal")[], // [] by default
chunksSize?: number, // 0 by default
considerMoveAsUpdate?: boolean; // false by default
}Browser
In a browser environment,
ReadableStreamrefers to the browser's streaming API, andFilerefers to an uploaded or local file. Examples are provided in the #usage section below.
prevList: ReadableStream<Record<string, unknown>> | File | Record<string, unknown>[],
nextList: ReadableStream<Record<string, unknown>> | File | Record<string, unknown>[],
referenceProperty: keyof Record<string, unknown>,
options: {
showOnly?: ("added" | "deleted" | "moved" | "updated" | "equal")[], // [] by default
chunksSize?: number, // 0 by default
considerMoveAsUpdate?: boolean; // false by default
}prevList: the original object list.nextList: the new object list.referenceProperty: a property common to all objects in your lists (e.g.id).optionschunksSizethe number of object diffs returned by each streamed chunk. (e.g.0= 1 object diff per chunk,10= 10 object diffs per chunk).showOnlygives you the option to return only the values whose status you are interested in (e.g.["added", "equal"]).considerMoveAsUpdate: if set totrueamovedvalue will be considered asupdated.
Output
The objects diff are grouped into arrays - called chunks - and are consumed thanks to an event listener. You have access to 3 events:
data: to be notified when a new chunk of object diffs is available.finish: to be notified when the stream is finished.error: to be notified if an error occurs during the stream.
interface StreamListener<T extends Record<string, unknown>> {
on<E extends keyof EmitterEvents<T>>(
event: E,
listener: Listener<EmitterEvents<T>[E]>,
): this;
}
type EmitterEvents<T extends Record<string, unknown>> = {
data: [StreamListDiff<T>[]];
error: [Error];
finish: [];
};
type StreamListDiff<T extends Record<string, unknown>> = {
currentValue: T | null;
previousValue: T | null;
prevIndex: number | null;
newIndex: number | null;
indexDiff: number | null;
status: "added" | "deleted" | "moved" | "updated" | "equal";
};USAGE
Input
You can send streams, file paths, or arrays as input:
If you are in a server environment
// for a simple array
const stream = [{ id: 1, name: "hello" }]
// for a large array
const stream = Readable.from(list, { objectMode: true });
// for a local file
const stream = path.resolve(__dirname, "./list.json");
If you are in a browser environment
// for a simple array
const stream = [{ id: 1, name: "hello" }]
// for a large array
const stream = new ReadableStream({
start(controller) {
list.forEach((value) => controller.enqueue(value));
controller.close();
},
});
// for a local file
const stream = new File([JSON.stringify(file)], "file.json", { type: "application/json" });
// for a file input
const stream = e.target.files[0];
Example
const diff = streamListDiff(
[
- { id: 1, name: "Item 1" },
{ id: 2, name: "Item 2" },
{ id: 3, name: "Item 3" }
],
[
+ { id: 0, name: "Item 0" },
{ id: 2, name: "Item 2" },
+ { id: 3, name: "Item Three" },
],
"id",
{ chunksSize: 2 }
);Output
diff.on("data", (chunk) => {
// first chunk received (2 object diffs)
[
+ {
+ previousValue: null,
+ currentValue: { id: 0, name: 'Item 0' },
+ prevIndex: null,
+ newIndex: 0,
+ indexDiff: null,
+ status: 'added'
+ },
- {
- previousValue: { id: 1, name: 'Item 1' },
- currentValue: null,
- prevIndex: 0,
- newIndex: null,
- indexDiff: null,
- status: 'deleted'
- }
]
// second chunk received (2 object diffs)
[
{
previousValue: { id: 2, name: 'Item 2' },
currentValue: { id: 2, name: 'Item 2' },
prevIndex: 1,
newIndex: 1,
indexDiff: 0,
status: 'equal'
},
+ {
+ previousValue: { id: 3, name: 'Item 3' },
+ currentValue: { id: 3, name: 'Item Three' },
+ prevIndex: 2,
+ newIndex: 2,
+ indexDiff: 0,
+ status: 'updated'
+ },
]
});
diff.on("finish", () => console.log("Your data has been processed. The full diff is available."))
diff.on("error", (err) => console.log(err))isEqual()
import { isEqual } from "@donedeal0/superdiff";Tests whether two values are equal.
FORMAT
Input
a: unknown,
b: unknown,
options: {
ignoreArrayOrder: boolean; // false by default
},a: the value to be compared to the valueb.b: the value to be compared to the valuea.ignoreArrayOrder: if set totrue,["hello", "world"]and["world", "hello"]will be treated asequal, because the two arrays contain the same values, just in a different order.
USAGE
isEqual(
[
{ name: "joe", age: 99 },
{ name: "nina", age: 23 },
],
[
{ name: "joe", age: 98 },
{ name: "nina", age: 23 },
],
);Output
false;isObject()
import { isObject } from "@donedeal0/superdiff";Tests whether a value is an object.
FORMAT
Input
value: unknown;value: the value whose type will be checked.
USAGE
Input
isObject(["hello", "world"]);Output
false;ℹ️ More examples are available in the source code tests.
CREDITS
DoneDeal0
SUPPORT
If you or your company uses Superdiff, please show your support by becoming a sponsor! Your name and company logo will be displayed on the README.md. Premium support is also available. https://github.com/sponsors/DoneDeal0
CONTRIBUTING
Issues and pull requests are welcome!