diagrams-js
diagrams-js/diagrams-js-plugin-system
feature>-
diagrams-js Plugin System
Extend diagrams-js with custom importers, exporters, metadata providers, and lifecycle hooks.
Quick Start
Creating a Simple Plugin
// Create a custom exporter plugin
// Note: Use context.lib to access diagrams-js exports instead of importing
const myExporter = () => ({
name: "my-exporter",
version: "1.0.0",
apiVersion: "1.0",
runtimeSupport: {
node: true,
browser: true,
deno: true,
bun: true,
},
capabilities: [
{
type: "exporter",
name: "my-format",
extension: ".my",
mimeType: "text/plain",
export: async (diagram, context) => {
// Access library exports via context.lib
const { Node, Edge } = context.lib;
return `Diagram: ${diagram.name}`;
},
},
],
});
// Use the plugin (import only needed for Diagram creation)
import { Diagram } from "diagrams-js";
const diagram = Diagram("My Architecture");
// Register plugins explicitly
await diagram.registerPlugins([myExporter]);
// Export to custom format
const result = await diagram.export("my-format");
console.log(result); // "Diagram: My Architecture"Built-in Plugins
Built-in plugins (JSON and SVG) are automatically registered when you call plugin-related methods:
const diagram = Diagram("My Architecture");
// Built-in JSON plugin auto-registers on first use
const json = await diagram.export("json"); // Works without explicit registration
const json2 = diagram.toJSON(); // Also works
// Built-in SVG plugin auto-registers on first use
const svg = await diagram.export("svg"); // Works without explicit registration
const restored = await Diagram.fromSVG(svg); // Re-import from SVGPlugin Capabilities
Importer
Import diagrams from external formats:
const dockerComposePlugin = () => ({
name: "docker-compose",
version: "1.0.0",
apiVersion: "1.0",
runtimeSupport: { node: true, browser: true, deno: true, bun: true },
capabilities: [
{
type: "importer",
name: "docker-compose",
extensions: [".yml", ".yaml"],
mimeTypes: ["text/yaml"],
canImport: async (source) => {
return source.includes("services:");
},
import: async (source, diagram, context) => {
// Use context.loadYaml() instead of bundling yaml parser
const yaml = await context.loadYaml();
const compose = yaml.load(source);
// Access Node from context.lib instead of importing
const { Node } = context.lib;
// Create nodes for each service
for (const [name, service] of Object.entries(compose.services)) {
const node = diagram.add(Node(name));
node.metadata = { image: service.image };
}
},
},
],
});
// Usage
const diagram = Diagram("My App");
await diagram.registerPlugins([dockerComposePlugin]);
// Import single source
await diagram.import(yamlContent, "docker-compose");
// Import multiple sources - each in its own cluster
await diagram.import([compose1, compose2, compose3], "docker-compose");Exporter
Export diagrams to external formats:
const terraformPlugin = () => ({
name: "terraform",
version: "1.0.0",
apiVersion: "1.0",
runtimeSupport: { node: true, browser: true, deno: true, bun: true },
capabilities: [
{
type: "exporter",
name: "terraform",
extension: ".tf",
mimeType: "text/plain",
export: async (diagram, context) => {
let tf = `# Terraform generated from ${diagram.name}\n\n`;
for (const node of diagram.toJSON().nodes) {
tf += `resource "aws_instance" "${node.id}" {\n`;
tf += ` # Configuration...\n`;
tf += `}\n\n`;
}
return tf;
},
},
],
});
// Usage
const terraform = await diagram.export("terraform");Metadata Provider
Attach cloud provider metadata to nodes:
const awsMetadataPlugin = () => ({
name: "aws-metadata",
version: "1.0.0",
apiVersion: "1.0",
runtimeSupport: { node: true, browser: true, deno: true, bun: true },
capabilities: [
{
type: "metadata",
provider: "aws",
nodeTypes: ["EC2", "RDS", "Lambda"],
getMetadata: async (nodeType, config, context) => {
// Fetch pricing/specs from AWS API
return {
provider: "aws",
specifications: {
cpu: 4,
memory: "16GB",
},
pricing: {
hourly: 0.192,
monthly: 140.16,
},
};
},
},
],
});
// Usage
const diagram = Diagram("My App");
await diagram.registerPlugins([awsMetadataPlugin]);
const ec2 = diagram.add(EC2("Web Server"));
// Attach metadata to all EC2 nodes
await diagram.attachMetadata("aws", "EC2");
// Access metadata
console.log(ec2.metadata.pricing.monthly); // 140.16Hooks
Execute code at lifecycle events:
const loggingPlugin = () => ({
name: "logging",
version: "1.0.0",
apiVersion: "1.0",
runtimeSupport: { node: true, browser: true, deno: true, bun: true },
capabilities: [
{
type: "hook",
hooks: [
{
event: "before:export",
handler: async (data, context) => {
console.log(`Exporting to ${data.format}...`);
return data;
},
},
{
event: "after:export",
handler: async (data, context) => {
console.log("Export complete!");
return data;
},
},
],
},
],
});SVG Post-Processing with after:layout
The after:layout hook fires after Graphviz renders the SVG but before format conversion to PNG/JPG. This is the ideal place for SVG string manipulation that should also affect raster output.
const svgPostProcessor = () => ({
name: "svg-post-processor",
version: "1.0.0",
apiVersion: "1.0",
runtimeSupport: { node: true, browser: true, deno: true, bun: true },
capabilities: [
{
type: "hook",
hooks: [
{
event: "after:layout",
handler: async ({ svg, diagram, format }) => {
// Modify SVG string - affects both SVG and PNG/JPG output
const modified = svg.replace(/stroke="#7b8894"/g, 'stroke="red"');
return { svg: modified, diagram, format };
},
},
],
},
],
});Available Hook Events
- before:import / after:import - Import operations
- before:export / after:export - Export operations
- before:render / after:render - Rendering
- after:layout - After Graphviz produces SVG, before format conversion (PNG/JPG)
- before:serialize / after:deserialize - JSON serialization
- node:create - Node creation
- edge:create - Edge creation
- cluster:create - Cluster creation
- metadata:attach - Metadata attachment
Plugin Dependencies
Plugins can declare dependencies on other plugins:
const advancedPlugin = () => ({
name: "advanced",
version: "1.0.0",
apiVersion: "1.0",
runtimeSupport: { node: true, browser: true, deno: true, bun: true },
dependencies: ["base-plugin"], // Depends on base-plugin
capabilities: [
{
type: "exporter",
name: "advanced-format",
extension: ".adv",
mimeType: "text/plain",
export: async (diagram, context) => {
// Can access base-plugin's capabilities
const baseExporter = context.getExporter("base-format");
const base = await baseExporter.export(diagram, context);
return `Advanced: ${base}`;
},
},
],
});
// Register in correct order (dependencies first)
const diagram = Diagram("Test");
await diagram.registerPlugins([basePlugin, advancedPlugin]);Plugin Registry
Access the plugin registry to query capabilities:
const diagram = Diagram("Test");
await diagram.registerPlugins([myPlugin]);
// List all capabilities
const caps = diagram.registry.listCapabilities();
console.log(caps.importers); // ["docker-compose", ...]
console.log(caps.exporters); // ["terraform", "json", ...]
// Get specific plugin
const importer = diagram.registry.getImporter("docker-compose");
const exporter = diagram.registry.getExporter("terraform");
const provider = diagram.registry.getMetadataProvider("aws");
// List all registered plugins
const plugins = diagram.registry.listPlugins();Best Practices
1. Use Factory Functions
Always create plugins as factory functions:
// ✅ Good
const myPlugin = (config) => ({
name: "my-plugin",
// ...
});
// ❌ Avoid
const myPlugin = {
name: "my-plugin",
// ...
};2. Declare Runtime Support
Always declare which runtimes your plugin supports:
runtimeSupport: {
node: true, // Works in Node.js
browser: false, // Doesn't work in browsers (uses fs)
deno: false, // Not tested in Deno
bun: false, // Not tested in Bun
}3. Use Context-Provided Utilities
For YAML parsing and other common needs, use the utilities provided in the plugin context instead of bundling your own dependencies:
// ✅ Good: Use context.loadYaml() - no bundling needed
import: async (source, diagram, context) => {
const yaml = await context.loadYaml();
const parsed = yaml.load(source);
// ...
}
// ❌ Avoid: Bundling your own YAML parser
import: async (source, diagram, context) => {
const yaml = await import("yaml"); // Adds bundle size
const parsed = yaml.parse(source);
// ...
}This approach:
- Reduces plugin bundle size
- Ensures consistent parsing across plugins
- Works across all runtimes automatically
4. Validate Configuration
Use requiredConfig and initialize:
const apiPlugin = () => ({
name: "api-plugin",
version: "1.0.0",
apiVersion: "1.0",
requiredConfig: ["apiKey", "region"],
async initialize(config, context) {
if (!config.apiKey) {
throw new Error("apiKey is required");
}
// Set up API client
},
// ...
});
// Usage with config
const diagram = Diagram("Test");
await diagram.registerPlugins([[apiPlugin, { apiKey: "xxx", region: "us-west-2" }]]);Common Patterns
Importer with YAML
const myImporter = () => ({
name: "my-format",
version: "1.0.0",
apiVersion: "1.0",
runtimeSupport: { node: true, browser: true, deno: true, bun: true },
capabilities: [
{
type: "importer",
name: "my-format",
extensions: [".yml", ".yaml"],
canImport: async (source) => source.includes("services:"),
import: async (source, diagram, context) => {
const yaml = await context.loadYaml();
const data = yaml.load(source);
const { Node } = context.lib;
for (const [name, service] of Object.entries(data.services)) {
diagram.add(Node(name)).metadata = { image: service.image };
}
},
},
],
});Metadata Provider
const awsMetadataPlugin = () => ({
name: "aws-metadata",
version: "1.0.0",
apiVersion: "1.0",
runtimeSupport: { node: true, browser: true, deno: true, bun: true },
capabilities: [
{
type: "metadata",
provider: "aws",
nodeTypes: ["EC2", "RDS", "Lambda"],
getMetadata: async (nodeType, config, context) => ({
provider: "aws",
pricing: { hourly: 0.192, monthly: 140.16 },
specifications: { cpu: 4, memory: "16GB" },
}),
},
],
});