/superdiff

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.

Primary LanguageTypeScript

superdiff-logo

CI CD NPM Downloads GitHub Tag


WHAT IS IT?

This library compares two arrays or objects and returns a full diff of their differences.

ℹ️ The documentation is also available on our website!


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!

AlexisAnzieu omonk sneko


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.
  • options
    • ignoreArrayOrder: if set to true, ["hello", "world"] and ["world", "hello"] will be treated as equal, 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:
          • basic returns only the main properties whose status matches your query.
          • deep can 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[];
};

type Diff = {
  property: string;
  previousValue: unknown;
  currentValue: unknown;
  status: "added" | "deleted" | "equal" | "updated";
  // recursive diff in case of subproperties
  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.
  • options
    • showOnly gives you the option to return only the values whose status you are interested in (e.g. ["added", "equal"]).
    • referenceProperty will consider an object to be updated rather than added or deleted if one of its properties remains stable, such as its id. This option has no effect on other datatypes.
    • ignoreArrayOrder: if set to true, ["hello", "world"] and ["world", "hello"] will be treated as equal, because the two arrays contain the same values, just in a different order.
    • considerMoveAsUpdate: if set to true a moved value will be considered as updated.

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.

ℹ️ streamListDiff requires ESM support for browser usage. It will work out of the box if you use a modern bundler (Webpack, Rollup) or JavaScript framework (Next.js, Vue.js).

FORMAT

Input

Server

In a server environment, Readable refers to Node.js streams, and FilePath refers 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
  useWorker?: boolean; // true by default
  showWarnings?: boolean; // true by default
}

Browser

In a browser environment, ReadableStream refers to the browser's streaming API, and File refers 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
  useWorker?: boolean; // true by default
  showWarnings?: boolean; // true 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).
  • options
    • chunksSize the number of object diffs returned by each streamed chunk. (e.g. 0 = 1 object diff per chunk, 10 = 10 object diffs per chunk).
    • showOnly gives you the option to return only the values whose status you are interested in (e.g. ["added", "equal"]).
    • considerMoveAsUpdate: if set to true a moved value will be considered as updated.
    • useWorker: if set to true, the diff will be run in a worker for maximum performance. Only recommended for large lists (e.g. +100,000 items).
    • showWarnings: if set to true, potential warnings will be displayed in the console.

⚠️ Warning: using Readable streams may impact workers' performance since they need to be converted to arrays. Consider using arrays or files for optimal performance. Alternatively, you can turn the useWorker option off.

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> {
  on(event: "data", listener: (chunk: StreamListDiff<T>[]) => void);
  on(event: "finish", listener: () => void);
  on(event: "error", listener: (error: Error) => void);
}

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 value b.
  • b: the value to be compared to the value a.
  • ignoreArrayOrder: if set to true, ["hello", "world"] and ["world", "hello"] will be treated as equal, 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


sponsor

CONTRIBUTING

Issues and pull requests are welcome!