Skip to content

zendive/jsdiff

Repository files navigation

JSDiff

An extension for developers that enhances the console API by incorporating the ability to compare objects and adds a JSDiff tab (parallel to Elements, Network panels) within your dev-tools for viewing the results.

Screenshots
  • Comparing two objects screenshot

  • Tracking changes in localStorage (unchanged are hidden) screenshot

Motivation

  • Track object mutations during runtime and/or while debugging with intention to find expected or unexpected changes.

Features

  • User interface:

    • Hide / show unchanged properties.
    • Copy changed properties in format of jsondiffpatch Delta object.
    • Clear current result.
    • Search input to highlight patterns.
      • If search query contains at least one upper-case letter - the search will be case-sensitive.
    • Indicator of the last update time.
    • Indicator of a fatal error (out of storage memory).
    • DevTools light/dark color scheme support.
  • Compare objects between multiple [sub]domains, Chrome tabs, or single page reloads.

    • JSDiff DevTools panel reflects current state of comparison, regardless the tab[s] it was opened from.
  • Fail-safe serialization of objects having security issues while accessing their properties or objects having toJSON() function; when instead of serialization of all object properties, - only toJSON() return value is serialized, like JSON.strigify() does.

  • Can be used from within online code editors like: codesandbox.io, coderpad.io, flems.io, codepen.io, jsfiddle.net, mdn playground.

Limitations

  • Map keys like 0 and '0' would be merged due to Map to Object conversion.

  • While paused in debug mode, JSDiff panel won't reflect the result until runtime is resumed (see #10).

  • Compared objects, after being serialized, stored in chrome.storage.local which has 10 MB limit.

  • In Firefox the API is under jsdiff object for now, cause extension API's not fully compatible.

API

  • console.diff(left, right) - compare left and right arguments
console.diff({ a: 1, b: 1, c: 3 }, { a: 1, b: 2, d: 3 });
  • console.diffPush(next) - shifts sides, right becomes left, next becomes right
console.diffPush(Date.now());
  • console.diff(next) - shorthand for diffPush
console.diff(Date.now());
  • console.diffLeft(left) - update the old value only
console.diffLeft(Date.now());
  • console.diffRight(right) - update the new value only
console.diffRight(Date.now());

Typescript

Global Console interface declaration for quick copy/paste when used from typescript:

declare global {
  interface Console {
    diff(left: unknown, right?: unknown): void;
    diffPush(next: unknown): void;
    diffLeft(left: unknown): void;
    diffRight(right: unknown): void;
  }
}

Serialization by types

Input Output
XMLHttpRequest[1] ƒ XMLHttpRequest⟪native⟫
function test(){}[1] ƒ test⟪1374b28d22b674e53a044425556a9cd48b82fd5aba3bf19e3545d51704227b10⟫
document.body {0001}[2,3] DOM⟪BODY⟫
±Infinity Number⟪±Infinity⟫
NaN Number⟪NaN⟫
98765432109876543210n BigInt⟪98765432109876543210⟫
void 0 ⟪undefined⟫
/example/i RegExp⟪/example/i⟫
new URL('https://example.com/') URL⟪https://example.com/⟫
Symbol('example') {0001}[3] Symbol(example)
Symbol.for('global') Symbol(global)
(obj = {key: 1}, {first: obj, second: obj}) {"first": {"key": 1}, "second": "[0002][4] Object⟪♻️⟫"}
(key2= {}, map = new Map([['key1', 1], [key2, 2]]), {first: map, second: map}) {"first": {"[0003][4,5] Object⟪♻️⟫": 2, "key1": 1}, "second": "[0002][4] Map⟪♻️⟫"}
(arr = [1], {first: arr, second: arr}) {"first": [1], "second": "[0002][4] Array⟪♻️⟫"}
(set = new Set([1]), {first: set, second: set}) {"first: [1], "second": "[0002][4] Set⟪♻️⟫"}

1 Functions included in comparison result in order to detect possible alterations, in form of a string combined from a function name (if present) and a hash of a function.toString() body.

2 DOM element serialized by pseudo id and nodeName.

3 Notation {} denotes pseudo id from a Set of unique instances, which is assigned during serialization of compared sides and remains inside internal WeakMap lookup catalog until its garbage collected or page is reloaded.

4 Notation [] denotes pseudo id from a Multiset of recurring instances, which is assigned in the scope of serialization of a high level argument instance, while comparing left or right side; that means - if some object, having id of [0001] on the left side, is not guarantied to have same id on the right side.

5 Map key, unless it's a primitive type, serialized by his pseudo id.

Protection

  • How to protect your site from this extension:

    • Tests in Chrome show that even Content-Security-Policy: default-src 'none'; header won't prevent injection of extension content-scripts...

    • Avoid assigning to window or globalThis any application object. See also accidental global variables and memory leaks.

    • In general, you can incapacitate console functions:

    for (const prop in console) {
      if (typeof console[prop] === 'function' && prop !== 'error') {
        console[prop] = function noop() {};
      }
    }

Build instructions

  • Linux
  • node 22.14 (LTS)
make install      # install dependencies
make all          # build for prod and make extension.${browser}.zip
make tune2chrome  # or tune2firefox for relevant manifest.json file
make dev          # local development

Based on

Communication schemes between Content-script and DevTools panel
  • Chrome mv3 screenshot
  • Firefox screenshot