UNPKG

diagrams-js

Version:

A TypeScript port of the diagrams Python library for drawing cloud system architecture diagrams as code

357 lines (259 loc) 10.3 kB
--- name: diagrams-js/diagram-diff description: > Visual diff for diagrams. Compare two versions of a diagram to see added, removed, and modified elements with color-coded side-by-side rendering. Perfect for code reviews, documentation, and architecture evolution tracking. type: core library: diagrams-js library_version: "0.5.0" requires: - diagrams-js/getting-started - diagrams-js/json-serialization sources: - "hatemhosny/diagrams-js:docs/docs/guides/diagram-diff.mdx" - "hatemhosny/diagrams-js:src/diff.ts" --- This skill builds on diagrams-js/getting-started and diagrams-js/json-serialization. Read them first for foundational concepts. # diagrams-js — Diagram Diff Compare two versions of a diagram visually. The diff feature highlights **added**, **removed**, and **modified** elements with color-coded overlays in a side-by-side view. ## Use Cases - **Code reviews**: See exactly what infrastructure changed in a PR - **Documentation**: Show evolution of system architecture - **Auditing**: Track what was added or removed between deployments ## Core Patterns ### Compute and Render a Diff ```typescript import { computeDiff, renderDiff } from "diagrams-js"; import { writeFileSync } from "fs"; const before = JSON.parse(await fs.readFile("arch-v1.json", "utf8")); const after = JSON.parse(await fs.readFile("arch-v2.json", "utf8")); // Compute the diff const diff = computeDiff(before, after); console.log(diff.summary); // { added: 2, removed: 1, modified: 3, unchanged: 5 } // Render as self-contained HTML const html = await renderDiff(diff, before, after, { format: "html" }); await fs.writeFile("diff.html", html); ``` ### Use Convenience Static Methods ```typescript import { Diagram } from "diagrams-js"; // Just compute const diff = Diagram.diff(before, after); // Compute and render in one call const html = await Diagram.renderDiff(before, after, { format: "html" }); ``` ### Understanding the Diff Result ```typescript const diff = computeDiff(before, after); // Summary counts console.log(diff.summary); // { added: 2, removed: 1, modified: 3, unchanged: 5 } // Check specific node const nodeDiff = diff.nodes.get("web-server"); if (nodeDiff?.kind === "modified") { console.log("Changes:", nodeDiff.changes); // ["label: \"Web\" → \"Web v2\"", "type: \"EC2\" → \"Lambda\""] } ``` ### Render Options ```typescript const html = await renderDiff(diff, before, after, { format: "html", // "html" or "svg" theme: "light", // "light" or "dark" layout: "side-by-side", // "side-by-side" or "stacked" showUnchanged: "show", // "show" (default), "dim", or "hide" showLegend: true, // Show color legend showSummary: true, // Show change counts hoverDetails: true, // Show tooltips on hover }); ``` ## Change Types | Kind | Color | Description | | ----------- | -------- | -------------------------------------------- | | `added` | 🟢 Green | New element in after version | | `removed` | 🔴 Red | Element deleted in after version | | `modified` | 🟠 Amber | Properties changed (including label changes) | | `unchanged` | ⚪ Gray | No changes (shown by default) | ## Node Matching Algorithm Nodes are matched using a three-phase approach: ### Phase 1: Fingerprint Matching Nodes with identical `(label, provider, type, resource)` are matched directly as `unchanged` or `modified`. ```typescript // Before: { label: "Web Server", provider: "aws", type: "compute", resource: "EC2" } // After: { label: "Web Server", provider: "aws", type: "compute", resource: "EC2" } // Result: unchanged // Before: { label: "Web Server", provider: "aws", type: "compute", resource: "EC2" } // After: { label: "Web Server v2", provider: "aws", type: "compute", resource: "EC2" } // Result: modified (label change) ``` ### Phase 2: Label Fingerprint + Edge Connectivity Unmatched nodes with the same `(provider, type, resource)` are matched using edge connectivity: - If edge connectivity patterns match → paired and marked as `modified` (label change) - If edge connectivity differs → treated as separate nodes (removed + added) ```typescript // Example: worker4 → worker1 with same connectivity // Before: { label: "worker4", provider: "aws", type: "EC2", connectsTo: ["lb"] } // After: { label: "worker1", provider: "aws", type: "EC2", connectsTo: ["lb"] } // Result: modified (label: "worker4" → "worker1") // Example: Different connectivity = different nodes // Before: { label: "worker4", provider: "aws", type: "EC2", connectsTo: ["lb"] } // After: { label: "worker1", provider: "aws", type: "EC2", connectsTo: ["db"] } // Result: removed (worker4) + added (worker1) ``` ### Phase 3: Simple Label Fingerprint Remaining unmatched nodes are matched 1:1 by `(provider, type, resource)`: - Same label → `unchanged` - Different labels → `modified` ```typescript // Before: { label: "worker1", provider: "aws", type: "EC2" } // After: { label: "worker2", provider: "aws", type: "EC2" } // Result: modified (label: "worker1" → "worker2") ``` ## Diff Options ```typescript const diff = computeDiff(before, after, { // Ignore layout/position changes (default: true) ignore: { position: true }, // Ignore all metadata ignore: { metadata: true }, // Ignore specific metadata keys ignore: { metadata: ["cpu", "memory"] }, // Ignore specific Graphviz attributes ignore: { attrs: ["color", "fillcolor"] }, // Custom matching function matchNodes: (a, b) => a.metadata?.resourceId === b.metadata?.resourceId, }); ``` ## Metadata Changes Diagram-level changes (name, theme, direction, etc.) are tracked separately: ```typescript const diff = computeDiff(before, after); if (diff.meta.name) { console.log(`Renamed: "${diff.meta.name.before}""${diff.meta.name.after}"`); } if (diff.meta.theme) { console.log(`Theme changed: ${diff.meta.theme.before} → ${diff.meta.theme.after}`); } ``` These appear in the HTML output under "Diagram Options Changed". ## CLI Tool Use `diagrams-diff-cli` for git workflows: ```bash # Install globally npm install -g diagrams-diff-cli # Compare with HEAD diagrams-diff HEAD diagram.json -o diff.html # Compare branches diagrams-diff main...feature diagram.json -o diff.html # Terminal preview diagrams-diff HEAD diagram.json --format terminal ``` ## Common Mistakes ### CRITICAL Forgetting to await renderDiff Wrong: ```typescript const html = renderDiff(diff, before, after); // Missing await fs.writeFileSync("diff.html", html); // html is Promise, not string ``` Correct: ```typescript const html = await renderDiff(diff, before, after); fs.writeFileSync("diff.html", html); ``` `renderDiff` is async because it renders both diagrams to SVG. ### HIGH Expecting modified nodes without matching criteria Wrong: ```typescript // Before: { label: "worker4", provider: "aws", type: "EC2" } // After: { label: "worker1", provider: "aws", type: "EC2" } // Different labels, no edge connectivity match → detected as removed + added ``` Correct: ```typescript // Before: { label: "worker1", provider: "aws", type: "EC2" } // After: { label: "worker1-prod", provider: "aws", type: "EC2" } // Same provider/type with matching connectivity → detected as modified ``` Keep the same `(provider, type, resource)` and similar edge connectivity for modification detection. ### MEDIUM Confusing modified with removed+added - `modified`: Same `(provider, type, resource)` with matching edge connectivity, but different label - `removed` + `added`: Different labels without matching connectivity, or different `(provider, type, resource)` ```typescript if (nodeDiff.kind === "modified") { // Label changed but same entity console.log("Renamed:", nodeDiff.changes); } if (nodeDiff.kind === "removed") { // Node was deleted console.log("Removed:", nodeDiff.before?.label); } if (nodeDiff.kind === "added") { // Node is new console.log("Added:", nodeDiff.after?.label); } ``` ## Best Practices ### Use Stable Node Identifiers Node matching relies on `(label, provider, type, resource)`, not IDs: ```typescript // Good: Consistent identifiers across versions const web = diagram.add(EC2("Web Server")); // Good: Meaningful labels that persist const api = diagram.add(Lambda("API Handler")); ``` ### Export JSON for Versioning Save `diagram.toJSON()` alongside your code: ```typescript // In your build/CI await diagram.save("architecture.json", { format: "json" }); ``` ### Focus on Meaningful Changes Ignore cosmetic changes: ```typescript computeDiff(before, after, { ignore: { position: true, // Layout changes attrs: ["color"], // Visual styling metadata: ["cost"], // Non-structural metadata }, }); ``` ## Complete Example ```typescript import { Diagram, computeDiff, renderDiff } from "diagrams-js"; import { EC2, Lambda } from "diagrams-js/aws/compute"; import { RDS } from "diagrams-js/aws/database"; import { S3 } from "diagrams-js/aws/storage"; // Version 1: Traditional architecture const v1 = Diagram("Architecture v1", { direction: "TB" }); const web1 = v1.add(EC2("Web Server")); const db1 = v1.add(RDS("Database")); web1.to(db1); // Version 2: Serverless migration const v2 = Diagram("Architecture v2", { direction: "TB" }); const web2 = v2.add(Lambda("API Handler")); // Changed: EC2 → Lambda const db2 = v2.add(RDS("Database")); // Unchanged const storage = v2.add(S3("Assets")); // Added web2.to(db2); web2.to(storage); // Compute diff const diff = computeDiff(v1.toJSON(), v2.toJSON()); console.log(diff.summary); // { added: 1, removed: 0, modified: 1, unchanged: 1 } // Render visual diff const html = await renderDiff(diff, v1.toJSON(), v2.toJSON(), { format: "html", theme: "light", showUnchanged: "show", // or "dim" to dim unchanged, "hide" to hide them }); await fs.writeFile("architecture-diff.html", html); ``` The HTML shows: - 🟢 Green: New S3 "Assets" node - 🟠 Amber: Changed EC2 → Lambda node - ⚪ Unchanged: RDS node (shown normally, use `showUnchanged: "dim"` to dim) ## See Also - diagrams-js/json-serialization — Export diagrams to JSON - diagrams-js/rendering-export — General rendering options - diagrams-js/diagram-configuration — Diagram options (name, theme, direction)