superdiff-logo [![CI](https://github.com/DoneDeal0/superdiff/actions/workflows/ci.yml/badge.svg)](https://github.com/DoneDeal0/superdiff/actions/workflows/ci.yml) [![CD](https://github.com/DoneDeal0/superdiff/actions/workflows/cd.yml/badge.svg)](https://github.com/DoneDeal0/superdiff/actions/workflows/cd.yml) ![NPM Downloads](https://img.shields.io/npm/dy/%40donedeal0%2Fsuperdiff?logo=npm) ![GitHub Tag](https://img.shields.io/github/v/tag/DoneDeal0/superdiff?label=latest%20release)
# 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**!
AlexisAnzieu omonk sneko

## FEATURES **Superdiff** exports 6 functions: ```ts // 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) // Similar to streamListDiff, but for browser use streamListDiffClient(prevList, nextList, referenceProperty) // Checks whether two values are equal isEqual(dataA, dataB) // Checks whether a value is an object isObject(data) ```
### getObjectDiff() ```js 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** ```ts prevData: Record; nextData: Record; 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** ```ts 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** ```diff 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** ```diff { 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() ```js import { getListDiff } from "@donedeal0/superdiff"; ``` Compares two arrays and returns a diff for each entry. Supports duplicate values, primitive values and objects. #### FORMAT **Input** ```ts 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** ```ts 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** ```diff getListDiff( - ["mbappe", "mendes", "verratti", "ruiz"], + ["mbappe", "messi", "ruiz"] ); ``` **Output** ```diff { 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() ```js // If you are in a server environment import { streamListDiff } from "@donedeal0/superdiff"; // If you are in a browser environment import { streamListDiffClient } from "@donedeal0/superdiff"; ``` Streams the diff of two object lists, ideal for large lists and maximum performance. #### FORMAT **Input** #### streamListDiff (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. ```ts // streamListDiff prevList: Readable | FilePath | Record[], nextList: Readable | FilePath | Record[], referenceProperty: keyof Record, options: { showOnly?: ("added" | "deleted" | "moved" | "updated" | "equal")[], // [] by default chunksSize?: number, // 0 by default considerMoveAsUpdate?: boolean; // false by default } ``` #### streamListDiffClient (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. ```ts prevList: ReadableStream> | File | Record[], nextList: ReadableStream> | File | Record[], referenceProperty: keyof Record, 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`). - `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`. **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. ```ts interface StreamListener> { on>( event: E, listener: Listener[E]>, ): this; } type EmitterEvents> = { data: [StreamListDiff[]]; error: [Error]; finish: []; }; type StreamListDiff> = { 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 ```ts // 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 ```ts // 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 ```diff 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 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)) ``` **Using `fetch`** A common use case would be to do a live diff against a stream, in order to avoid loading the entire dataset into memory. Here are two examples, for browser and server use: Browser: ```ts import { streamListDiffClient } from "@donedeal0/superdiff"; async function streamDiffFromAPI() { try { const response = await fetch("https://example.com/api/streaming-data"); const reader = response.body.getReader(); const stream = new ReadableStream({ async start(controller) { let result; while (!(result = await reader.read()).done) { controller.enqueue(result.value); // Push the next chunk into the stream } controller.close(); // Close the stream when done }, }); const prevStream = [{ id: 1, name: "Joe" }, { id: 2, name: "Jane" }] // Some previous list or stream const diff = streamListDiffClient(prevStream, stream, 'id', { chunksSize: 5 }); diff.on("data", (diffChunk) => console.log(diffChunk)); diff.on("finish", () => console.log("Stream diff complete")); } catch (err) { console.error(err); } } ``` Server: ```ts import fetch from "node-fetch"; import { Readable } from "stream"; import { streamListDiff } from "@donedeal0/superdiff"; async function streamDiffFromAPI() { try { const response = await fetch("https://example.com/api/streaming-data"); const reader = response.body.getReader(); const stream = new Readable({ async read() { let result; while (!(result = await reader.read()).done) { this.push(result.value); // Push the next chunk into the stream } this.push(null); // Close the stream when done }, }); const prevList = [{ id: 1, name: "Joe" }, { id: 2, name: "Jane" }]; // Some previous list or stream const prevListStream = Readable.from(prevList, { objectMode: true }) const diff = streamListDiff(prevListStream, stream, 'id', { chunksSize: 5 }); diff.on("data", (diffChunk) => console.log(diffChunk)); diff.on("finish", () => console.log("Stream diff complete")); } catch (err) { console.error(err); } } ```
### isEqual() ```js import { isEqual } from "@donedeal0/superdiff"; ``` Tests whether two values are equal. #### FORMAT **Input** ```ts 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 ```ts isEqual( [ { name: "joe", age: 99 }, { name: "nina", age: 23 }, ], [ { name: "joe", age: 98 }, { name: "nina", age: 23 }, ], ); ``` **Output** ```ts false; ```
### isObject() ```js import { isObject } from "@donedeal0/superdiff"; ``` Tests whether a value is an object. #### FORMAT **Input** ```ts value: unknown; ``` - `value`: the value whose type will be checked. #### USAGE **Input** ```ts isObject(["hello", "world"]); ``` **Output** ```ts 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!