servly-sync
v0.2.2
Published
Bidirectional sync engine for Servly — diff, patch, hash, and conflict resolution for Layout JSON, props, and design tokens
Maintainers
Readme
servly-sync
Bidirectional sync engine for keeping design systems and code in sync. Diff, patch, hash, and resolve conflicts across Figma, code editors, CLIs, and AI tools.
Stack-agnostic. Zero dependencies. Works in Node.js, browsers, and edge runtimes.
Install
npm install servly-syncQuick start
import { diff, applyPatch, hashComponent } from 'servly-sync';
// 1. Diff two versions of a component layout
const result = diff(
[{ i: 'btn-1', componentId: 'Button', className: 'px-4 py-2' }],
[{ i: 'btn-1', componentId: 'Button', className: 'px-6 py-3' }],
);
console.log(result.hasChanges); // true
console.log(result.summary); // { added: 0, removed: 0, modified: 1, moved: 0 }
console.log(result.patch); // [{ op: 'replace', path: '/0/className', value: 'px-6 py-3' }]
// 2. Apply the patch to get the updated version
const updated = applyPatch(
[{ i: 'btn-1', componentId: 'Button', className: 'px-4 py-2' }],
result.patch,
);
// 3. Hash the component for change detection
const hash = hashComponent({
layout: updated,
propsSchema: [{ name: 'label', type: 'string' }],
});API
Diff engine
Compare two values and produce RFC 6902 JSON Patch operations.
import { diff, diffLayout, diffProps, diffTokens, deepEqual } from 'servly-sync';
// General-purpose diff (objects, arrays, primitives)
const result = diff(before, after);
// result.hasChanges — boolean
// result.patch — JSONPatchOperation[]
// result.changedPaths — string[]
// result.summary — { added, removed, modified, moved }
// Domain-specific diffs (same output, scoped base paths)
const layoutDiff = diffLayout(oldLayout, newLayout); // base: /layout
const propsDiff = diffProps(oldProps, newProps); // base: /propsSchema
const tokensDiff = diffTokens(oldTokens, newTokens); // base: /tokens
// Deep equality check
deepEqual({ a: 1 }, { a: 1 }); // trueCustom array identity
By default, arrays are diffed by matching items on i, id, or name fields, falling back to index. Override this when your data model uses different ID fields:
// Use any field as the array item key
const result = diff(before, after, {
getKey: (item: any) => item.uuid,
});
// Works with all diff functions
const layoutDiff = diffLayout(oldLayout, newLayout, {
getKey: (item: any) => item.nodeId,
});Patch application
Apply JSON Patch operations (RFC 6902) to any value. Returns a new deep-cloned result — the original is never mutated.
import { applyPatch, validatePatch, PatchError } from 'servly-sync';
const updated = applyPatch(document, [
{ op: 'replace', path: '/title', value: 'New Title' },
{ op: 'add', path: '/tags/-', value: 'new-tag' },
{ op: 'remove', path: '/deprecated' },
]);
// Validate without applying
const error = validatePatch(document, operations);
if (error) console.error(error.message);Supports all six RFC 6902 operations: add, remove, replace, move, copy, test.
Hashing
DJB2-based hashing for change detection. Compatible with the Figma plugin's hashing, so hashes match across all sync surfaces.
import {
hashValue,
hashLayout,
hashProps,
hashTokens,
hashComponent,
hashComponentParts,
DEFAULT_LAYOUT_FIELDS,
} from 'servly-sync';
// Hash individual parts
const layoutHash = hashLayout(elements);
const propsHash = hashProps(propsSchema);
const tokensHash = hashTokens(tokens);
// Hash an entire component snapshot
const componentHash = hashComponent({
layout: elements,
propsSchema,
tokens,
metadata: { version: '1.0' },
});
// Get per-part hashes to know *which* part changed
const parts = hashComponentParts({ layout: elements, propsSchema });
// { layout: 'a3f2c1b0', props: '7e1d4f2a', tokens: '00000000', metadata: '00000000' }Custom layout fields
By default, hashLayout hashes 9 structural fields. Use custom fields when your component model is different:
// Hash only the fields your component model uses
const hash = hashLayout(elements, ['key', 'type', 'children', 'props']);
// Extend the defaults with your own fields
const hash = hashLayout(elements, [...DEFAULT_LAYOUT_FIELDS, 'customField']);Conflict detection & resolution
Detect and resolve conflicts when two sources modify the same component concurrently.
import {
detectConflict,
detectAllConflicts,
resolveConflict,
mergeChanges,
} from 'servly-sync';
// Detect a single conflict between two change events
const conflict = detectConflict(localChange, remoteChange);
// Detect all conflicts between sets of changes
const conflicts = detectAllConflicts(localChanges, remoteChanges);
// Resolve with a strategy
const resolution = resolveConflict(conflict, 'auto-merge');
// Strategies: 'local-wins' | 'remote-wins' | 'auto-merge' | 'manual'
// Full merge pipeline — resolves what it can, surfaces the rest
const { merged, conflicts: unresolved, resolutions } = mergeChanges(
baseDocument,
localChanges,
remoteChanges,
'auto-merge',
);Sync pipeline
High-level orchestration that coordinates diff, hash, conflict, and patch into a single call. Use these when you want the full sync workflow without manually calling each primitive.
import { preparePush, preparePull, prepareMerge, applyResolution } from 'servly-sync';
// Push: diff local against remote, get patches to send
const pushResult = preparePush(localSnapshot, remoteSnapshot);
// pushResult.patches — JSONPatchOperation[] to apply on remote
// pushResult.hash — hash of local state
// Pull: diff remote against local, get patches to apply locally
const pullResult = preparePull(localSnapshot, remoteSnapshot);
// Merge: when both sides changed, detect and resolve conflicts
const mergeResult = prepareMerge(baseSnapshot, localSnapshot, remoteSnapshot, {
strategy: 'auto-merge',
diffOptions: { getKey: (item: any) => item.uid },
});
// mergeResult.patches — resolved patches
// mergeResult.conflicts — unresolved conflicts (if strategy is 'manual')
// Apply resolved patches to a snapshot
const updated = applyResolution(baseSnapshot, mergeResult.patches);Sync state management
Track the sync status of each component across local and remote sources.
import {
createSyncState,
recordLocalChange,
recordRemoteChange,
computeStatus,
markSynced,
describeSyncState,
SyncStateStore,
} from 'servly-sync';
// Functional API
let state = createSyncState('btn-primary', initialHash);
state = recordLocalChange(state, changeEvent);
console.log(computeStatus(state)); // 'ahead'
console.log(describeSyncState(state)); // '1 local change(s) to push'
state = markSynced(state, newHash);
console.log(computeStatus(state)); // 'in-sync'
// Class-based store for managing multiple components
const store = new SyncStateStore();
store.recordLocal('btn-primary', changeEvent);
store.recordRemote('card-hero', remoteEvent);
store.getByStatus('conflict'); // components with conflicts
store.getAllConflicts(); // all unresolved conflictsStatuses: in-sync | ahead | behind | diverged | conflict | unknown
Change events
Create and manage sync change events that flow through the system.
import {
createChangeEvent,
createBatch,
chunkBatches,
validateChangeEvent,
groupByComponent,
groupByChangeType,
sortEvents,
} from 'servly-sync';
// Split large event lists into sized batches for rate-limited APIs
const batches = chunkBatches('cli', 'my-tool', events, 50);
// batches[0].changes.length <= 50Sources: figma | builder | cli | cursor | ai
Change types: layout | props | tokens | style | metadata | component | behavior
Design token sync
Transform design tokens between Figma Variables, CSS custom properties, and Tailwind config.
import {
figmaVariablesToServlyTokens,
servlyTokensToCSS,
servlyTokensToCSSWithModes,
servlyTokensToTailwindConfig,
servlyTokensToTailwindConfigString,
cssToServlyTokens,
buildTokenSyncManifest,
DEFAULT_CATEGORY_RULES,
} from 'servly-sync';
// Figma Variables -> Servly tokens
const { tokens, entries } = figmaVariablesToServlyTokens(figmaVariables);
// Servly tokens -> CSS custom properties
const css = servlyTokensToCSS(tokens);
// :root {
// --ds-primary: #3b82f6;
// --ds-spacing-sm: 8px;
// }
// With light/dark mode support
const cssWithModes = servlyTokensToCSSWithModes(tokens);
// Servly tokens -> Tailwind config
const tailwindConfig = servlyTokensToTailwindConfig(tokens);
// CSS -> Servly tokens (reverse sync)
const parsedTokens = cssToServlyTokens(existingCSS);Custom category inference
Control how token categories are inferred from variable names:
// Add custom category rules (checked before built-in rules)
const { tokens } = figmaVariablesToServlyTokens(variables, {
prefix: 'my',
categoryRules: [
{ keywords: ['motion', 'duration', 'animation'], category: 'motion' },
{ keywords: ['breakpoint'], category: 'responsive' },
],
});
// Or override category inference entirely
const { tokens } = figmaVariablesToServlyTokens(variables, {
inferCategory: (variable) => myCustomCategoryLogic(variable),
});Props mapper
Bidirectional prop type mapping between design tool types and code types. Ships with a Figma preset — create your own for Sketch, Adobe XD, Penpot, or custom component models.
import {
mapPropType,
reversePropType,
mapPropValue,
extendPreset,
FIGMA_PROP_PRESET,
} from 'servly-sync';
// Map Figma prop types to servly-sync types
mapPropType('TEXT', FIGMA_PROP_PRESET); // 'string'
mapPropType('VARIANT', FIGMA_PROP_PRESET); // 'enum'
mapPropType('BOOLEAN', FIGMA_PROP_PRESET); // 'boolean'
// Reverse map back to Figma types
reversePropType('string', FIGMA_PROP_PRESET); // 'TEXT'
// Create a custom preset for your design tool
const sketchPreset = extendPreset(FIGMA_PROP_PRESET, 'sketch', [
{ sourceType: 'COLOR', targetType: 'color' },
{ sourceType: 'TEXT', targetType: 'string', transformValue: (v) => String(v).trim() },
]);Format converter registry
Register converters between component representations. Each registry is independent — no global state.
import { createFormatRegistry } from 'servly-sync';
const registry = createFormatRegistry();
// Register a Figma -> Snapshot converter
registry.register({
from: 'figma-json',
to: 'snapshot',
convert: (figmaNode) => ({
componentId: figmaNode.id,
hash: '',
timestamp: new Date().toISOString(),
layout: extractLayout(figmaNode),
}),
});
// Convert
const snapshot = registry.convert('figma-json', 'snapshot', figmaNode);
// List registered converters
registry.list(); // [['figma-json', 'snapshot']]Semantic tag inference
Infer HTML semantic tags from component names and structure. 130+ built-in rules, extensible with your own.
import { inferTag, inferTagsForLayout, getTagRules } from 'servly-sync';
inferTag('Button'); // { tag: 'button', role: 'button', confidence: 0.95 }
inferTag('Hero Image'); // { tag: 'img', confidence: 0.8 }
inferTag('Navbar'); // { tag: 'nav', role: 'navigation', confidence: 0.95 }
// Add custom rules for your component library
inferTag('Widget', {
customRules: [
{ pattern: 'widget', tag: 'section', role: 'region', confidence: 0.95 },
],
});
// Replace built-in rules entirely
inferTag('Button', {
customRules: myRules,
mergeWithDefaults: false,
});Source adapter interface
Define extraction/application adapters for any design tool or code framework. The interface lives in servly-sync — implementations live in your code.
import type { SourceAdapter, ComponentSnapshot } from 'servly-sync';
class FigmaAdapter implements SourceAdapter<FigmaNode> {
name = 'figma';
async extract(): Promise<ComponentSnapshot[]> { /* ... */ }
async apply(patches: JSONPatchOperation[]): Promise<void> { /* ... */ }
toSnapshot(node: FigmaNode): ComponentSnapshot { /* ... */ }
fromSnapshot(snapshot: ComponentSnapshot): FigmaNode { /* ... */ }
}Notifications
Generate user-facing notifications from sync events.
import {
notificationFromChange,
conflictNotification,
prCreatedNotification,
filterNotifications,
} from 'servly-sync';Architecture
servly-sync is the shared core consumed by every surface in the Servly ecosystem:
Figma Plugin ──┐
Code Editor ──┤
CLI ──┼── servly-sync ── diff/patch/hash/conflict ── Any Backend
AI Tools ──┤
MCP Server ──┘The sync pipeline:
- Diff — Compare two component snapshots, produce JSON Patch ops
- Hash — Fingerprint components for fast change detection
- Conflict — Detect overlapping changes from concurrent edits
- Resolve — Apply a strategy (auto-merge, local-wins, remote-wins, manual)
- Patch — Apply the resolved operations to produce the final state
Development
# Install dependencies
npm install
# Build (ESM + CJS + types)
npm run build
# Run tests
npm test
# Watch mode
npm run devProject structure
src/
index.ts Public API exports
types.ts All TypeScript type definitions
diff.ts RFC 6902 diff engine (configurable array keying)
patch.ts JSON Patch application
hash.ts DJB2 hashing (configurable layout fields)
conflict.ts Conflict detection & resolution
syncState.ts Sync state management
changeEvent.ts Change event creation + batch chunking
pipeline.ts Sync orchestration (push/pull/merge)
tokenSync.ts Design token transformations (configurable categories)
propsMapper.ts Bidirectional prop type mapping
formatRegistry.ts Format converter registry
fingerprint.ts Component fingerprinting
semanticTags.ts Semantic tag inference (extensible rules)
notifications.ts Notification generation
__tests__/ Test suite (Vitest)Running tests
npm test # single run
npm run test:watch # watch modeContributing
We welcome contributions. Here's how to get started:
- Fork the repo and create a branch from
main - Install dependencies:
npm install - Make your changes and add tests
- Run the test suite:
npm test - Run the build:
npm run build - Submit a pull request
What we're looking for
- Bug fixes with a failing test case
- Performance improvements to the diff/patch engine
- New conflict resolution strategies
- Additional design token format support (Style Dictionary, Tokens Studio, etc.)
- Better array diffing algorithms (LCS-based, move detection)
- Source adapter implementations for design tools (Sketch, Adobe XD, Penpot)
- Props mapper presets for additional tools
- Documentation improvements and examples
Conventions
- TypeScript strict mode
- No production dependencies — keep the bundle lean
- Comments explain decision-making, not obvious code
- All public functions need JSDoc comments
- Tests use Vitest
- Builds use tsup (ESM + CJS dual output)
What's being built
Servly is building an open standard for syncing design systems between tools and code. servly-sync is the engine at the center of that — it handles the hard parts of bidirectional sync so that every integration surface (Figma plugins, CLI tools, editor extensions, AI agents) speaks the same language.
Roadmap
- Component Interchange Format (CIF) — A JSON spec for describing components in a tool-agnostic way.
servly-syncwill validate and transform CIF documents. - Framework wrappers — React, Vue, Svelte, and Angular adapters that consume CIF and render native components.
- Move detection in diffs — Detect when array items are reordered, not just added/removed.
- Three-way merge — Use a common ancestor for smarter conflict resolution.
- Operational transform — Real-time collaborative editing across sync sources.
- Token format adapters — Import/export from Style Dictionary, Tokens Studio, and other token formats.
- Built-in source adapters — First-party adapters for Figma, Sketch, and code frameworks.
License
MIT
