jsdiff

repository·master·Indexed 27 days ago

https://github.com/kpdecker/jsdiff

A JavaScript text diff implementation based on the Myers (1986) algorithm. It provides tools to compute differences between strings at various granularities—including characters, words, lines, sentences, CSS, and JSON—and supports the creation, parsing, and application of unified diff patches.

Tokens
6.1K
Snippets
8
Records
35
Agent score
92%

What's inside jsdiff

  1. Understand how jsdiff functions work

    master

    jsdiff functions follow a three-step process to compare an old text and a new text:

    1. Tokenization: Both texts are split into arrays of "tokens" (e.g., characters in diffChars, lines in diffLines).
    2. Difference Calculation: The algorithm finds the smallest set of single-token insertions and deletions to transform the first array into the second. Equality between tokens is typically determined by ===, but can be configured (e.g., using {ignoreCase: true}).
    3. Result Generation: An array of change objects is returned, ordered from the start to the end of the input. Each object represents an insertion, a deletion, or a kept set of tokens.
  2. Apply complex Git patches (renames, copies, and mode changes)

    master

    When applying Git patches, use applyPatches with a pattern that handles file renames and deletions using a pendingWrites Map. This prevents issues with file swaps (e.g., a -> b and b -> a) and ensures renames are handled correctly.

    Key properties available on the patch object:

    • patch.isRename: Boolean indicating a rename.
    • patch.isCopy: Boolean indicating a copy.
    • patch.isDelete: Boolean indicating a deletion.
    • patch.isCreate: Boolean indicating a new file creation.
    • patch.oldFileName: The original file path (often prefixed with a/ in Git).
    • patch.newFileName: The new file path (often prefixed with b/ in Git).
    • patch.newMode: The new file mode/permissions.
    const {applyPatches} = require('diff');
    const fs = require('fs'); // Note: fs must be required
    const patch = fs.readFileSync("git-diff.patch").toString();
    const DELETE = Symbol('delete');
    const pendingWrites = new Map(); // filePath → {content, mode} or DELETE sentinel
    applyPatches(patch, {
        loadFile: (patch, callback) => {
            if (patch.isCreate) {
                // Newly created file — no old content to load
                callback(undefined, '');
                return;
            }
            try {
                // Git diffs use a/ and b/ prefixes; strip them to get the real path
                const filePath = patch.oldFileName.replace(/^a\//, '');
                callback(undefined, fs.readFileSync(filePath).toString());
            } catch (e) {
                callback(`No such file: ${patch.oldFileName}`);
            }
        },
        patched: (patch, patchedContent, callback) => {
            if (patchedContent === false) {
                callback(`Failed to apply patch to ${patch.oldFileName}`);
                return;
            }
            const oldPath = patch.oldFileName.replace(/^a\//, '');
            const newPath = patch.newFileName.replace(/^b\//, '');
            if (patch.isDelete) {
                if (!pendingWrites.has(oldPath)) {
                    pendingWrites.set(oldPath, DELETE);
                }
            } else {
                pendingWrites.set(newPath, {content: patchedContent, mode: patch.newMode});
                // For renames, delete the old file (but not for copies, 
                // where the old file should be kept)
                if (patch.isRename && !pendingWrites.has(oldPath)) {
                    pendingWrites.set(oldPath, DELETE);
                }
            }
            callback();
        },
        complete: (err) => {
            if (err) {
                console.log("Failed with error:", err);
                return;
            }
            for (const [filePath, entry] of pendingWrites) {
                if (entry === DELETE) {
                    fs.unlinkSync(filePath);
                } else {
                    fs.writeFileSync(filePath, entry.content);
                    if (entry.mode) {
                        fs.chmodSync(filePath, entry.mode.slice(-3));
                    }
                }
            }
        }
    });
  3. Import jsdiff in ESM or CommonJS

    master

    You can import specific functions directly from the diff package depending on your module system.

    // ESM
    import {diffChars, createPatch} from 'diff';
    
    // CommonJS
    const {diffChars, createPatch} = require('diff');
  4. Use TypeScript with jsdiff

    master

    JsDiff includes built-in type definitions (since version 8). Do not install @types/diff, as it may conflict with the built-in types.

    Note on Overload Signatures: Functions like diffChars use TypeScript overloads to handle different modes:

    1. Abortable mode: Triggered by passing timeout or maxEditLength in the options. The return type may be undefined.
    2. Async mode: Triggered by providing a callback in the options. The return value is always undefined, and results are passed to the callback.

    To avoid type errors, ensure you pass an object literal as the options argument so TypeScript can statically determine the correct overload. Avoid building options objects programmatically (e.g., const options: any = {}) as this may cause TypeScript to default to the non-async, non-abortable signature.

  5. Use jsdiff in a web page without a module system

    master
    To use jsdiff in a browser without a module bundler, include dist/diff.js or dist/diff.min.js in your HTML. This creates a global Diff object containing the entire JsDiff API.
  6. Apply a multi-file patch in Node.js

    master

    Use applyPatches to apply a patch that affects multiple files. You must provide a configuration object with the following callbacks:

    • loadFile(patch, callback): Called to load the content of the file being patched. patch.oldFileName provides the path.
    • patched(patch, patchedContent, callback): Called after a patch is successfully applied to a file's content. Use this to write the patchedContent to disk.
    • complete(err): Called when all patches have been processed.
    const {applyPatches} = require('diff');
    const fs = require('fs'); // Note: fs must be required
    const patch = fs.readFileSync("mydiff.patch").toString();
    applyPatches(patch, {
        loadFile: (patch, callback) => {
            let fileContents;
            try {
                fileContents = fs.readFileSync(patch.oldFileName).toString();
            } catch (e) {
                callback(`No such file: ${patch.oldFileName}`);
                return;
            }
            callback(undefined, fileContents);
        },
        patched: (patch, patchedContent, callback) => {
            if (patchedContent === false) {
                callback(`Failed to apply patch to ${patch.oldFileName}`)
                return;
            }
            fs.writeFileSync(patch.oldFileName, patchedContent);
            callback();
        },
        complete: (err) => {
            if (err) {
                console.log("Failed with error:", err);
            }
        }
    });
  7. Generate a patch file from two files in Node.js

    master

    Use createTwoFilesPatch to generate a unified diff patch between two sets of file contents. This is functionally equivalent to the Unix diff -u command.

    const {createTwoFilesPatch} = require('diff');
    const fs = require('fs'); // Note: fs must be required
    const file1Contents = fs.readFileSync("file1.txt").toString();
    const file2Contents = fs.readFileSync("file2.txt").toString();
    const patch = createTwoFilesPatch("file1.txt", "file2.txt", file1Contents, file2Contents);
    fs.writeFileSync("mydiff.patch", patch);
  8. Perform character-level diffing in Node.js

    master

    Use diffChars to compare two strings. The returned array contains objects representing parts of the strings. Each part has an added boolean (true if the text was added), a removed boolean (true if the text was removed), and a value string containing the text content.

    require('colors');
    const {diffChars} = require('diff');
    
    const one = 'beep boop';
    const other = 'beep boob blah';
    
    const diff = diffChars(one, other);
    
    diff.forEach((part) => {
      // green for additions, red for deletions
      let text = part.added ? part.value.bgGreen :
                 part.removed ? part.value.bgRed :
                                part.value;
      process.stderr.write(text);
    });
    
    console.log();
  9. Perform character-level diffing in a web page

    master

    In a browser environment, include diff.js via a <script> tag. Access the Diff global object to call Diff.diffChars(one, other). The resulting array can be used to manipulate the DOM, such as creating <span> elements with different colors for additions, deletions, or common text.

    <pre id="display"></pre>
    <script src="diff.js"></script>
    <script>
    const one = 'beep boop',
        other = 'beep boob blah',
        color = '';
        
    let span = null;
    
    const diff = Diff.diffChars(one, other),
        display = document.getElementById('display'),
        fragment = document.createDocumentFragment();
    
    diff.forEach((part) => {
      // green for additions, red for deletions
      // grey for common parts
      const color = part.added ? 'green' :
        part.removed ? 'red' : 'grey';
      span = document.createElement('span');
      span.style.color = color;
      span.appendChild(document
        .createTextNode(part.value));
      fragment.appendChild(span);
    });
    
    display.appendChild(fragment);
    </script>
  10. Apply a single patch to a file in Node.js

    master

    Use applyPatch to apply a patch string to a specific file's content. This is functionally equivalent to the Unix patch command.

    const {applyPatch} = require('diff');
    const fs = require('fs'); // Note: fs must be required
    const file1Contents = fs.readFileSync("file1.txt").toString();
    const patch = fs.readFileSync("mydiff.patch").toString();
    const patchedFile = applyPatch(file1Contents, patch);
    fs.writeFileSync("file1.txt", patchedFile);
  11. Parse a unified diff patch with parsePatch()

    master

    Use parsePatch(diffStr) to convert a unified diff string into a structured patch object. This method supports Git's specific dialect and provides additional fields for Git patches:

    • isGit: true if it's a Git-style patch.
    • isRename: true if the file was renamed.
    • isCopy: true if the file was copied.
    • isCreate: true if it's a new file.
    • isDelete: true if it's a deleted file.
    • oldMode / newMode: File modes (e.g., '100644').
    • isBinary: true if it's a binary file change.