CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-diff

A JavaScript text diff implementation based on the Myers algorithm for comparing text at different granularities.

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

json-diffing.mddocs/

JSON Diffing

JSON object comparison with pretty-printing and custom serialization options. Automatically serializes objects to prettified JSON format and performs line-based comparison for readable diff output.

Capabilities

diffJson Function

Performs JSON-aware diff between two objects by serializing them to pretty-printed JSON.

/**
 * Compare two JSON-serializable objects
 * @param oldObj - Original object or JSON string
 * @param newObj - New object to compare against
 * @param options - Configuration options
 * @returns Array of change objects representing the diff
 */
function diffJson(oldObj, newObj, options);

Options:

interface JsonDiffOptions extends DiffOptions {
  stringifyReplacer?: (key: string, value: any) => any;  // Custom JSON.stringify replacer
  undefinedReplacement?: any;  // Value to replace undefined properties
}

Usage Examples:

import { diffJson } from "diff";

// Basic object comparison
const result = diffJson(
  { name: "Alice", age: 25, city: "New York" },
  { name: "Alice", age: 26, city: "Boston" }
);
console.log(result);
// Shows line-by-line diff of pretty-printed JSON

// Array comparison  
const arrayResult = diffJson(
  { items: [1, 2, 3] },
  { items: [1, 2, 3, 4] }
);

// Nested object comparison
const nestedResult = diffJson(
  {
    user: {
      profile: { name: "John", settings: { theme: "dark" } }
    }
  },
  {
    user: {
      profile: { name: "John", settings: { theme: "light", lang: "en" } }
    }
  }
);

canonicalize Function

Recursively canonicalizes objects for consistent JSON serialization, handling circular references.

/**
 * Canonicalize object for consistent JSON comparison
 * @param obj - Object to canonicalize
 * @param stack - Internal recursion stack (optional)
 * @param replacementStack - Internal replacement stack (optional)
 * @param replacer - Custom replacer function (optional)
 * @param key - Current property key (optional)
 * @returns Canonicalized object with sorted keys
 */
function canonicalize(obj, stack, replacementStack, replacer, key);

jsonDiff Instance

Pre-configured Diff instance for JSON comparisons with specialized handling.

/**
 * Pre-configured JSON diff instance
 * Uses line-based tokenization of pretty-printed JSON
 * Includes longest token preference for better diff quality
 */
const jsonDiff: Diff;

Advanced Usage

Custom Serialization

import { diffJson } from "diff";

// Custom replacer function
const result = diffJson(
  { date: new Date("2023-01-01"), value: undefined },
  { date: new Date("2023-01-02"), value: null },
  {
    stringifyReplacer: (key, value) => {
      if (value instanceof Date) {
        return value.toISOString();
      }
      if (value === undefined) {
        return "<undefined>";
      }
      return value;
    }
  }
);

// Undefined replacement
const undefinedResult = diffJson(
  { a: 1, b: undefined, c: 3 },
  { a: 1, b: 2, c: undefined },
  {
    undefinedReplacement: null
  }
);

Object Preprocessing

import { diffJson, canonicalize } from "diff";

// Manual canonicalization
const obj1 = { b: 2, a: 1, c: { z: 26, y: 25 } };
const obj2 = { a: 1, b: 3, c: { y: 25, z: 26 } };

const canonical1 = canonicalize(obj1);
const canonical2 = canonicalize(obj2);
console.log("Canonicalized objects have consistent property order");

// Direct comparison with canonicalized objects
const result = diffJson(canonical1, canonical2);

API Response Comparison

import { diffJson } from "diff";

function compareApiResponses(response1, response2) {
  // Filter out volatile fields before comparison
  const cleaned1 = cleanResponse(response1);
  const cleaned2 = cleanResponse(response2);
  
  return diffJson(cleaned1, cleaned2, {
    stringifyReplacer: (key, value) => {
      // Ignore timestamp fields
      if (key.includes('timestamp') || key.includes('time')) {
        return '<timestamp>';
      }
      // Normalize IDs
      if (key === 'id' && typeof value === 'string') {
        return '<id>';
      }
      return value;
    }
  });
}

function cleanResponse(response) {
  const { timestamp, requestId, ...cleaned } = response;
  return cleaned;
}

// Usage
const oldResponse = {
  status: "success",
  data: { users: [{ id: "123", name: "Alice" }] },
  timestamp: "2023-01-01T10:00:00Z"
};

const newResponse = {
  status: "success", 
  data: { users: [{ id: "456", name: "Alice" }, { id: "789", name: "Bob" }] },
  timestamp: "2023-01-01T10:05:00Z"
};

const apiDiff = compareApiResponses(oldResponse, newResponse);

Configuration Comparison

import { diffJson } from "diff";

function compareConfigurations(oldConfig, newConfig) {
  const changes = diffJson(oldConfig, newConfig);
  
  const analysis = {
    addedKeys: [],
    removedKeys: [],
    modifiedValues: [],
    unchangedLines: 0
  };
  
  changes.forEach(change => {
    if (change.added) {
      const keyMatch = change.value.match(/^\s*"([^"]+)":/);
      if (keyMatch) {
        analysis.addedKeys.push(keyMatch[1]);
      }
    } else if (change.removed) {
      const keyMatch = change.value.match(/^\s*"([^"]+)":/);
      if (keyMatch) {
        analysis.removedKeys.push(keyMatch[1]);
      }
    } else {
      analysis.unchangedLines++;
    }
  });
  
  return analysis;
}

// Usage for configuration management
const oldConfig = {
  database: { host: "localhost", port: 5432 },
  cache: { enabled: true, ttl: 3600 }
};

const newConfig = {
  database: { host: "prod-db", port: 5432, ssl: true },
  cache: { enabled: false, ttl: 7200 },
  logging: { level: "info" }
};

const configAnalysis = compareConfigurations(oldConfig, newConfig);

Large Object Handling

import { diffJson } from "diff";

function diffLargeObjects(obj1, obj2, callback) {
  diffJson(obj1, obj2, {
    callback: callback,
    maxEditLength: 15000,  // Suitable for large JSON objects
    timeout: 25000,        // 25 second timeout
    stringifyReplacer: (key, value) => {
      // Potentially exclude large nested objects
      if (key === 'largeData' && Array.isArray(value) && value.length > 1000) {
        return `<large array: ${value.length} items>`;
      }
      return value;
    }
  });
}

// Usage
diffLargeObjects(largeObject1, largeObject2, (result) => {
  if (result) {
    const changeLines = result.filter(r => r.added || r.removed).length;
    console.log(`JSON diff completed: ${changeLines} changed lines`);
  } else {
    console.log("Objects too different to compute JSON diff efficiently");
  }
});

Circular Reference Handling

import { diffJson, canonicalize } from "diff";

// The library automatically handles circular references
const objWithCircular = { name: "test" };
objWithCircular.self = objWithCircular;

const anotherObj = { name: "test", other: "value" };

// This works safely - circular references are handled
const result = diffJson(objWithCircular, anotherObj);

// Manual canonicalization also handles circularity
const canonical = canonicalize(objWithCircular);
console.log("Circular reference handled:", canonical);

Direct Instance Usage

import { jsonDiff } from "diff";

// Using the pre-configured instance
const directResult = jsonDiff.diff(
  { old: "value" },
  { new: "value" }
);

// Access the JSON serialization behavior
const serialized = jsonDiff.castInput({ b: 2, a: 1 });
console.log("Serialized JSON:", serialized);
// Pretty-printed JSON with sorted keys

// Custom equality (handles trailing commas in JSON)
const isEqual = jsonDiff.equals(
  '  "key": "value",',
  '  "key": "value"'
);
console.log("Ignores trailing comma differences:", isEqual);

Data Migration Tracking

import { diffJson } from "diff";

function trackDataMigration(beforeMigration, afterMigration) {
  const diff = diffJson(beforeMigration, afterMigration);
  
  const migrationReport = {
    totalChanges: diff.filter(d => d.added || d.removed).length,
    dataIntegrityIssues: [],
    schemaChanges: [],
    valueTransformations: []
  };
  
  diff.forEach(change => {
    if (change.added || change.removed) {
      const line = change.value.trim();
      
      // Detect schema changes
      if (line.includes('"type":') || line.includes('"schema":')) {
        migrationReport.schemaChanges.push({
          change: change.added ? 'added' : 'removed',
          content: line
        });
      }
      
      // Detect potential data loss
      if (change.removed && line.includes(':') && !line.includes('null')) {
        migrationReport.dataIntegrityIssues.push({
          type: 'potential_data_loss',
          content: line
        });
      }
    }
  });
  
  return migrationReport;
}

Testing and Validation

import { diffJson } from "diff";

function validateJsonChanges(expected, actual) {
  const changes = diffJson(expected, actual);
  
  const validation = {
    isMatch: changes.every(change => !change.added && !change.removed),
    differences: changes.filter(change => change.added || change.removed),
    summary: {
      additions: changes.filter(c => c.added).length,
      removals: changes.filter(c => c.removed).length
    }
  };
  
  return validation;
}

// Usage in tests
const expectedResult = { status: "success", count: 10 };
const actualResult = { status: "success", count: 12 };
const validation = validateJsonChanges(expectedResult, actualResult);

if (!validation.isMatch) {
  console.log("Test failed with differences:", validation.differences);
}

Install with Tessl CLI

npx tessl i tessl/npm-diff

docs

array-diffing.md

character-diffing.md

css-diffing.md

custom-diffing.md

format-conversion.md

index.md

json-diffing.md

line-diffing.md

patch-application.md

patch-creation.md

patch-utilities.md

sentence-diffing.md

word-diffing.md

tile.json