A synchronization layer that answers "is this generated component still aligned with its Figma node?" — without being a code generator itself.
Plain ESM JavaScript, zero runtime dependencies, Node ≥ 18. Ships a figma-sync
CLI plus a small library API.
npm install --save-dev @artcom/figma-syncOr, when developing against a sibling checkout:
{ "devDependencies": { "@artcom/figma-sync": "file:../figma-sync" } }npm install then exposes the figma-sync binary (in node_modules/.bin, so
it's callable from your package.json scripts) and the library entry
import { loadProject } from "@artcom/figma-sync".
The tool reads/writes a .figma-sync/ directory in the current working
directory (the consuming project), so run it from that project's root.
-
Mapping — components carry a metadata comment:
// figma-sync: file=dSRhp9PHg4w9Gnco2R19dg node=2267:1644 name=StatusCard -
Manifest —
.figma-sync/manifest.jsonrecords, per file, the design hash and code hash at generation time (update-manifestwrites it). The canonicalized design document is kept in.figma-sync/baselines/sodiffcan show what changed, not only that something changed. -
Design hash — the Figma node is reduced to an allowlist of design properties (name, type, geometry, fills, typography, …; ids and timestamps are dropped, and hidden subtrees are excluded), serialized as canonical JSON with sorted keys, and hashed (SHA-256).
-
Code hash — SHA-256 over the source with normalized line endings. Regions between
// figma-sync-ignore-startand// figma-sync-ignore-endare excluded, so handwritten logic there never counts as drift. -
Status — compares current design + code hashes against the manifest:
SYNC,DESIGN_CHANGED,CODE_CHANGED,BOTH_CHANGED,NODE_DELETED,FILE_DELETED,UNMAPPED,UNKNOWN. Exit code is non-zero on any drift, sofigma-sync statusworks as a CI gate. Output is colour-coded (no emoji) and colours auto-disable when piped orNO_COLORis set.
Access to Figma is pluggable (lib/backends.js):
rest(default) — live Figma REST API; needs aFIGMA_TOKENenv var, sostatusreflects the current design rather than a stale snapshot.figma-sync capturerefreshes all snapshots from it. WhenFIGMA_TOKENis not set, commands print a warning and fall back to thesnapshotbackend so they still run offline (explicit--backend=restandcapturestill hard-require the token).snapshot— reads committed node JSON from.figma-sync/snapshots/<fileKey>/<nodeId>.json. Works offline and in CI.
figma-sync scan # list annotated components → nodes
figma-sync update-manifest # record current state as the baseline
figma-sync status # drift report (non-zero exit on drift)
figma-sync diff StatusCard # what changed in the design since baseline
figma-sync capture # refresh snapshots from the REST API
figma-sync doctor # validate manifest/annotations/snapshotsTypically wired into the consuming project's package.json:
{ "scripts": { "figma:status": "figma-sync status" } }Library API (what the CLI wraps):
import { loadProject } from "@artcom/figma-sync"
const project = await loadProject()
const status = await project.status()
const diff = await project.diff("StatusCard").figma-sync/config.json in the consuming project:
{ "backend": "rest", "srcDir": "src" }designFields (the allowlist hashed for the design) can also be overridden
there; see lib/config.js for the default list.
FIGMA_TOKEN (for the rest backend) is read from the environment. The CLI
also auto-loads a .env file from the working directory (built-in, Node ≥
20.12), so the token can live in the consuming project's gitignored .env. A
real shell env var still takes precedence.
See docs/token-setup.md for how to create the token and
exactly which scopes it needs (short answer: only file_content:read).
- JavaScript, not TypeScript; comment scanning uses a regex, not an AST.
- Design freshness is pull-based; nothing watches Figma. A
figmaVersionfield plus the Figma versions/webhooks API would makeDESIGN_CHANGEDdetection live instead of capture-time. - No tests yet; the module boundaries (mapping / backends / hash / status / reporters) are where plugin interfaces would go.