@sanity/diff-patch
Generates conflict-resistant Sanity patch mutations by diffing two JSON documents or values.
Repository Health
Technical Analysis
@sanity/diff-patch computes the set of Sanity patch mutations needed to transform one document (or arbitrary value) into another. Rather than diffing structurally and emitting a single blunt set per changed field, it walks strings, arrays, and objects with type-specific strategies designed for real-time, collaborative editing: string edits use character-level diff-match-patch operations where safe, arrays with _key-tagged items are diffed by key (not index) so reordering and concurrent edits merge cleanly, and everything is batched into the smallest practical number of mutation operations.
It is used internally by Sanity’s App SDK to power per-keystroke collaborative document editing, but the two exported functions — diffPatch for full documents and diffValue for bare values — work standalone in any code that needs to turn a before/after pair into Sanity’s patch format, with optional optimistic-locking via ifRevisionID.
What You Get
diffPatch()for diffing whole Sanity documents into ready-to-send mutation arrays, including automatic_idextraction and optionalifRevisionIDoptimistic lockingdiffValue()for diffing bare values/subtrees with an optionalbasePathprefix, for cases outside full-document patching- Key-aware array diffing (
_key-tagged items) that generates reorder/insert/unset patches instead of blunt index-based rewrites, so concurrent array edits merge without clobbering each other - Automatic string diffing via
@sanity/diff-match-patchwith tuned size/change-ratio heuristics that fall back to plainsetfor large replacements, keeping keystroke-level edits fast - A typed
DiffErrorwith path/value context for invalid Sanity document shapes (bad key names, multi-dimensional arrays, immutable_typechanges) - Zero configuration required and zero network calls — pure, synchronous, dependency-light computation you can run anywhere
Common Use Cases
- Powering real-time collaborative editing in a custom Sanity Studio input component, generating patches per keystroke
- Building an offline-first or optimistic-update flow that reconciles local document edits into Sanity mutations before sending them
- Writing a migration or sync script that computes minimal patches between two versions of a dataset’s documents instead of overwriting them wholesale
- Implementing undo/redo or diffing tooling for editorial review flows that need to show or replay exact document changes
Under The Hood
Architecture
The library is a small, purely functional module with no classes or DI container: src/diffPatch.ts holds the core recursive diffItem dispatcher that routes to diffString, diffArray (itself splitting into key-based vs index-based strategies), and diffObject based on the runtime type of the compared values, threading a path and mutable patches accumulator through the recursion; src/patches.ts defines the internal Patch union and public SanityPatchOperations types; src/paths.ts, src/validate.ts, and src/setOperations.ts hold supporting path-formatting, Sanity-key validation, and set-arithmetic helpers; src/index.ts re-exports the two public entry points (diffPatch, diffValue) plus DiffError and types. A final serializePatches pass batches same-type operations (merging consecutive set/diffMatchPatch ops, batching unset paths, anchor-based insert batching, and a two-phase temp-key reorder strategy that avoids _key collisions during array swaps) into the fewest Sanity mutation objects possible. Because every consumer (Sanity Studio, the App SDK) funnels through this single diffItem dispatch, it is the one abstraction that would ripple outward if changed.
Tech Stack
Strict TypeScript compiled to dual ESM/CJS output (dist/index.js + dist/index.cjs plus .d.ts) via @sanity/pkg-utils (Vite-based), published as an ESM-only (type: module) package. The only runtime dependency is @sanity/diff-match-patch, Sanity’s own Myers-diff implementation used for character-level string diffing. Tests run on Vitest with @vitest/coverage-v8; linting is ESLint with eslint-config-sanity plus typescript-eslint and a tsc --noEmit check chained into posttest. Releases are cut with semantic-release from conventional commits, gated by a GitHub Actions matrix across Node 18/20/22 with an optional live integration-test job against a real Sanity project when repo secrets are configured. There is no server or database — this is a pure, synchronous computation library.
Code Quality
An extensive test suite (11 files under test/, plus snapshot fixtures) covers primitive, object, and array diffing, portable-text-shaped nested arrays, diff-match-patch string behavior, ifRevisionID locking, validation/safeguard edge cases, and full integration snapshots. posttest chains ESLint and a type-check so lint/type errors fail CI the same as test failures. Errors are explicit and typed via a custom DiffError class carrying path and value context rather than being swallowed silently. Naming is consistent and descriptive (diffItem, diffArrayByKey, serializePatches), typing is used throughout with no loose any beyond a few deliberately narrow casts, and non-obvious heuristics (the diff-match-patch size/ratio thresholds, the two-phase reorder algorithm) carry dense explanatory comments.
API Design
The public surface is deliberately tiny — diffPatch for documents, diffValue for bare values — with JSDoc on every exported symbol doubling as generated API reference (@public/@internal tags throughout). Getting started needs no configuration: diffPatch(source, target) alone returns ready-to-send mutations. Its real strength is encoded domain knowledge rather than novel general-purpose diffing: _key-addressed array diffing for collaboration-safe reordering, skipping diff-match-patch for system keys and for large-ratio string replacements, and exposing ifRevisionID optimistic locking directly in the options object — a lot of Sanity-specific collaborative-editing behavior behind a two-function API.