| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
tree-patch is a TypeScript library for immutable, conflict-aware editing of tree-shaped content.
It is designed for workflows like localization, CMS overlays, and content customization where you want to:
The runtime has zero Node/Bun-specific dependencies. The built library is intended for modern JavaScript runtimes, including browsers.
Install the package from npm:
npm install @hexie/tree-patchThen import from the package root:
import {
createDocument,
patchBuilder,
createEditor,
applyPatch,
validatePatch,
materialize,
preparePatch,
createResolutionSession,
diffTrees,
rebasePatch,
} from "@hexie/tree-patch";An input revision is treated as an opaque external revision and is preserved when applying a patch makes no semantic change. Changed snapshots receive a deterministic tree:h3: revision derived from content, explicit visibility, patch ownership, and document metadata.
Content, subtree, and path hashes carry an h3: algorithm prefix. Hash guards are intentionally version-specific: a persisted guard produced by a different hash algorithm conflicts safely instead of being interpreted as the current digest.
These 128-bit hashes are deterministic, non-cryptographic fingerprints. Child hashes use an incrementally updatable additive aggregate. Guards therefore provide probabilistic conflict detection for trusted CMS/localization workflows; they are not intended to resist adversarial collision construction.
import {
applyPatch,
createDocument,
patchBuilder,
type TreeDocument,
} from "@hexie/tree-patch";
type ContentTypes = {
Page: {};
Hero: {
title: string;
image: {
url: string;
alt?: string;
};
};
RichText: {
html: string;
};
};
const sourceDocument: TreeDocument<ContentTypes> = {
revision: "rev-1",
root: {
id: "root",
type: "Page",
attrs: {},
children: [
{
id: "hero",
type: "Hero",
attrs: {
title: "Summer Sale",
image: {
url: "/img/en.png",
},
},
children: [],
},
{
id: "legal",
type: "RichText",
attrs: {
html: "<p>US only</p>",
},
children: [],
},
],
},
};
const source = createDocument(sourceDocument);
const patch = patchBuilder<ContentTypes>({ source })
.patchId("fr-home")
.node("hero", "Hero")
.set(["title"], "Promotions d'ete", {
expect: "Summer Sale",
})
.set(["image", "url"], "/img/fr.png", {
expect: "/img/en.png",
})
.hideNode("legal")
.build();
const result = applyPatch(source, patch, { includeHidden: false });
if (result.status === "applied") {
console.log(result.tree.revision);
console.log(result.materialized.children[0]?.attrs);
}There are two authoring styles:
Field edits are intentionally node-scoped:
const patch = patchBuilder<ContentTypes>({ source })
.patchId("promo-flow")
.node("hero", "Hero")
.set(["title"], "Localized title")
.set(["image", "url"], "/img/fr.png")
.hideNode("legal")
.build();Using the editor:
const editor = createEditor(source, { patchId: "promo-flow" });
editor.node("hero", "Hero").set(["title"], "Localized title");
editor.node("legal", "RichText").hide();
const patch = editor.build();When a source tree is available, set() and remove() automatically guard the current field value (or its absence). An explicit expectation also validates the builder state immediately:
editor.node("hero", "Hero").set(["subtitle"], "Limited offer", {
expectAbsent: true,
});Use { unguarded: true } only when last-writer-wins behavior is intentional. Builders without a source cannot infer automatic guards, so expect and expectAbsent remain available for hand-authored patches.
Numeric builder path segments retain their array intent when intermediate containers are absent:
editor.node("page", "Page").set(
["sections", 0, "title"],
"Introduction",
{ expectAbsent: true },
);This creates sections as an array and its first item as an object. A string segment such as "0" remains an object property instead. The optional SetAttrOp.pathKinds wire field preserves this distinction because JSON pointers alone cannot distinguish an array index from a numeric object key before the container exists. Autovivified and existing empty arrays can be initialized at index 0; sparse array creation is rejected.
const validation = validatePatch(source, patch, { mode: "preview" });
const applied = applyPatch(source, patch, {
mode: "atomic",
includeHidden: false,
});
const materialized = materialize(source, patch, {
mode: "preview",
includeHidden: true,
});Behavior summary:
The nested result.materialized view and a changed snapshot's derived tree.revision are computed on first access and then cached. Callers that only need the indexed snapshot do not pay for either full-tree traversal.
const base = createDocument(sourceDocument);
const target = createDocument({
...sourceDocument,
root: {
...sourceDocument.root,
children: sourceDocument.root.children.map((child) =>
child.id === "hero"
? {
...child,
attrs: {
...child.attrs,
title: "Promotions d'ete",
},
}
: child,
),
},
});
const patch = diffTrees(base, target);
const rebased = rebasePatch(base, target, patch);diffTrees() generates deterministic semantic ops. rebasePatch() replays a patch on a new base and keeps only the operations that still apply cleanly.
For localization-style workflows, you can create a resolution session, make per-conflict decisions, and rebuild a fresh patch against the new base.
const session = createResolutionSession(oldBase, newBase, patch);
for (const conflict of session.unresolvedConflicts) {
if (conflict.opId === "hero-title") {
session.keepLocal(conflict.opId);
} else {
session.takeBase(conflict.opId);
}
}
const result = session.build();
if (result.status === "resolved") {
const nextPatch = result.resolvedPatch;
}Notes:
Use a TreeSchema when you need custom equality, hashing, cloning, or persistence for runtime values.
import { createDocument, patchBuilder, type TreeSchema } from "@hexie/tree-patch";
type ContentTypes = {
Hero: {
title: string;
publishedAt?: Date;
};
};
const dateCodec = {
codecId: "date",
serialize(value: Date) {
return value.toISOString();
},
deserialize(value: string) {
return new Date(value);
},
};
const schema: TreeSchema<ContentTypes> = {
types: {
Hero: {
adapters: {
"/publishedAt": {
equals: (left: Date, right: Date) => left.getTime() === right.getTime(),
clone: (value: Date) => new Date(value.getTime()),
hash: (value: Date) => value.toISOString(),
codec: dateCodec,
},
},
},
},
};Notes:
Patch ownership is provenance, not editable content. It participates in derived revisions and controls which structural operations are legal, but diffTrees() does not emit operations whose only effect would be changing ownership. Applying such a diff preserves the base tree's ownership state.
Typed attribute paths include the empty path for replacing the complete attrs value and are inferred up to eight segments deep. Removing the complete attribute root is intentionally excluded because removeAttr requires a field or array element.
Main functions:
Main exported types:
The library runtime does not depend on Node-only or Bun-only APIs.
It uses standard modern JavaScript features such as:
That makes it suitable for modern browsers, Bun, Deno, and Node ESM environments. The npm package declares Node 18 or newer. Older browsers may require transpilation or polyfills.
bun run build
bun run typecheck
bun testThe compiled package is emitted to dist/.
The implementation currently covers the planned v1 feature set of the library.
| Back | FazBrowse Home | New Git URL |