Skip to content

diagrams-js

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

diagrams-js/diagrams-js-plugin-system

feature
470 linesSource

>-

diagrams-js Plugin System

Extend diagrams-js with custom importers, exporters, metadata providers, and lifecycle hooks.

Quick Start

Creating a Simple Plugin

ts
// 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:

ts
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 SVG

Plugin Capabilities

Importer

Import diagrams from external formats:

ts
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:

ts
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:

ts
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.16

Hooks

Execute code at lifecycle events:

ts
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.

ts
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:

ts
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:

ts
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:

ts
// ✅ Good
const myPlugin = (config) => ({
  name: "my-plugin",
  // ...
});

// ❌ Avoid
const myPlugin = {
  name: "my-plugin",
  // ...
};

2. Declare Runtime Support

Always declare which runtimes your plugin supports:

ts
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:

ts
// ✅ 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:

ts
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

ts
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

ts
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" },
      }),
    },
  ],
});

Further Reading