npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ai-matrx/diff

v0.3.7

Published

The AI Matrx diff product: a line-level LCS text engine, a structural tree/array engine, and the complete React viewer — split/inline/highlight views, word-level highlights, folding, per-hunk accept/reject merging, and the structured entity diff with its

Readme

@ai-matrx/diff

The complete AI Matrx diff product: two pure engines plus the React viewer built on them.

  • @ai-matrx/diff/react — the whole UI. DiffViewer (the core everything else composes), TextDiff, DiffReview (per-hunk merge), InlineTextDiff, AnimatedDiffReveal, and the structured entity viewer DiffViewerShell with its field adapters. No icon library, no UI kit, no editor dependency.

  • The engines below are pure logic, zero dependencies, no DOM, nothing at import time:

  • @ai-matrx/diff/text — line-level LCS text diff. One computation emits both the unified (inline) and side-by-side (rows) representations, with word/char-level highlight segments on changed line pairs and a hunk model that turns the diff into an accept/reject merge tool.

  • @ai-matrx/diff/structural — generic tree/array structural diff. Deep-diffs two plain objects into a tree of typed change nodes with pluggable per-field identity keys for array matching.

The root (@ai-matrx/diff) re-exports both engines. /react is a separate subpath so a headless consumer never pays for the UI.

npm install @ai-matrx/diff

React viewer

import { DiffViewer, DiffReview } from "@ai-matrx/diff/react";

// Read-only. Fills its container: render it as a route, a window, a modal, a
// sheet, or any region of a page.
<DiffViewer
  original={a}
  modified={b}
  engine="auto"          // "auto" | "light" | "monaco"
  language="typescript"  // drives auto selection; omit for prose/markdown
  originalLabel="Before"
  modifiedLabel="After"
  defaultView="split"    // "split" | "inline" | "highlight"
/>

// Merge. Every hunk is pending / applied / rejected; Apply hands back the text.
<DiffReview original={a} modified={b} onApply={(merged) => save(merged)} />

Views: split (side-by-side), inline (unified), highlight (single-pane — the new document as flowing prose with only the changed regions tinted; the reader's view, not a code diff). Unchanged context folds to a few lines around each change and expands per run; Prev/Next jump between changes; Swap flips which side is the baseline. The diff is computed ONCE, so resolving, expanding and navigating never re-run the LCS.

Structured (entity) diff

import { computeDiff } from "@ai-matrx/diff/structural";
import {
  DiffViewerShell,
  createAdapterRegistry,
  TextFieldAdapter,
  TagsFieldAdapter,
} from "@ai-matrx/diff/react";

const adapters = createAdapterRegistry();
adapters.register("instructions", TextFieldAdapter);
adapters.register("tags", TagsFieldAdapter);

<DiffViewerShell
  diffResult={computeDiff(oldRecord, newRecord)}
  oldValue={oldRecord}
  newValue={newRecord}
  oldLabel="v3"
  newLabel="v4"
  adapters={adapters}
  temporalMetadata={dates}  // optional per-side / per-field provenance
/>

Fields with no registered adapter fall back to the package's DefaultFieldAdapter — never dropped. Provenance rows are explicit about what they do not know (loading, unavailable) and never substitute a record-wide timestamp for a missing field date.

Styling — the token contract

Components consume colour ONLY through the semantic token vocabulary (--card, --muted-foreground, --border, --primary, --success, --destructive, …). Two host steps:

/* Tailwind v4: compile the package's utilities. */
@source "../node_modules/@ai-matrx/diff/dist";
// Fresh app with no token vocabulary of its own — gives every token a
// sensible light/dark value. Apps that already define the AI Matrx tokens
// must NOT import this.
import "@ai-matrx/diff/styles.css";

Red-means-removed / green-means-added is deliberate and non-configurable (it is the universal diff convention, not a brand colour); everything else — surfaces, borders, text, fills — follows the host's tokens.

The one injection seam: a heavy (code editor) renderer

The package carries no code-editor dependency. Register one and DiffViewer routes source code and very large inputs to it:

import { setHeavyDiffRenderer, MONACO_DIFF_EDITOR_OPTIONS } from "@ai-matrx/diff/react";

setHeavyDiffRenderer(MyMonacoDiff); // once, at module scope

With none registered the light engine renders a complete diff — that is the working default, not a stand-in. Only an explicit engine="monaco" that cannot be honoured is degraded, and it says so in the toolbar and through onEngineResolved.

Text engine

import { computeTextDiff, applyHunks } from "@ai-matrx/diff/text";

const diff = computeTextDiff("the quick fox\nsecond line", "the slow fox\nsecond line");

diff.stats;          // { additions: 0, deletions: 0, modifications: 1, unchanged: 1 }
diff.inline;         // unified view: one entry per line per side (removed, then added)
diff.rows;           // split view: one aligned row per pair, left/right cells
diff.rows[0].left.segments;
// word-level highlights on the modified pair:
// [{ type: "unchanged", value: "the " }, { type: "removed", value: "quick" },
//  { type: "unchanged", value: " fox" }]
diff.whitespaceOnly; // true when only whitespace differs

// Merge tool: accept/reject hunks, get the merged text back.
// Accept-all === modified, accept-none === original — exactly.
applyHunks(original, modified, [0, 2]); // accept hunks 0 and 2, keep the rest old

Behavior notes: removed/added lines in the same change block pair up as modifications (with intra-line segments when the pair is at least 25% similar); inputs too large for the dense LCS matrix degrade safely to a whole-block replace instead of OOMing; ignoreTrailingWhitespace is a display option — the hunk/merge model always diffs raw lines so merges round-trip byte-exactly.

Structural engine

import { computeDiff, filterChangesDeep } from "@ai-matrx/diff/structural";

const result = computeDiff(
  { title: "Old", items: [{ id: "a", qty: 1 }, { id: "b", qty: 2 }] },
  { title: "New", items: [{ id: "b", qty: 2 }, { id: "a", qty: 5 }] },
  { identityKeys: { items: "id" } }, // or (item, index) => string
);

result.hasChanges; // true
result.stats;      // { added, removed, modified, unchanged, total }
result.root;       // DiffNode tree: title modified; items modified with children:
                   //   "a" modified (qty 1 → 5), "b" reordered
filterChangesDeep(result.root); // prune everything unchanged

Behavior notes: object key order is ignored by default (opt in per path via orderSensitiveObjectPaths); underscore-prefixed keys are compared by default — schema contract keys like __kind are data (skipUnderscorePrefix: true is an explicit opt-in presentation filter); heterogeneous arrays ([{...}, "str"], nested arrays, nulls) never throw — a mismatched pair is a modified leaf; when a configured identity key is not unique, the engine falls back to lossless positional matching rather than silently dropping items; excludePaths takes bare names for root fields or dotted paths for nested ones.

Design

  • Pure engines, dual ESM/CJS with matching type declarations, verified with a packed-tarball install loading every entry point through both import and require. The /react chunks carry the "use client" boundary; the engine entries deliberately do not, so they stay legal inside a React Server Component. The tarball gate checks both directions.
  • Both engines run happily during render; every quadratic path is capped and degrades instead of throwing or hanging.
  • Strict types throughout: exactOptionalPropertyTypes, noUncheckedIndexedAccess, zero any.
  • react / react-dom are OPTIONAL peers — only /react needs them.

License

MIT

Review sessions

DiffReview requires every hunk to be accepted or rejected before Apply. For a host-owned session, create it from @ai-matrx/diff/text, pass session and an onTransition reducer boundary; otherwise pass original and modified for the source-keyed internal session. Set disabled while the host is pending.