diagrams-js
diagrams-js/creating-plugins
feature482 linesSource
>-
Creating diagrams-js Plugins
Create custom plugins to extend diagrams-js with import/export formats, metadata providers, and hooks. Plugins enable integration with infrastructure-as-code tools, CI/CD pipelines, and cloud providers.
Plugin Architecture
Core Principles
- Never import from diagrams-js - Access runtime exports via context.lib to avoid multiple library instances. Type imports are allowed
- Use context.loadResourcesList() - Dynamically discover provider icons and resources
- Use context.loadYaml() - Parse and serialize YAML without bundling dependencies
- Convert to/from JSON - Use diagrams-js JSON format as the intermediary for imports
- Factory Functions - You may create plugins as factory functions if configuration may be needed
Package Structure
plaintext
plugin-my-format/
├── package.json # Plugin metadata and dependencies
├── tsconfig.json # TypeScript configuration
├── vite.config.ts # Build configuration
├── README.md # Documentation
├── src/
│ └── index.ts # Plugin implementation
├── tests/
│ └── index.test.ts # Test suite
└── skills/
└── plugin-my-format/
└── SKILL.md # AI agent skill documentationQuick Start
Minimal Plugin Package
ts
// src/index.ts
import type {
DiagramsPlugin,
ImporterCapability,
ExporterCapability,
Diagram,
ImportContext,
ExportContext,
} from "diagrams-js";
export interface MyPluginConfig {
/** Optional configuration */
format?: string;
}
export function createMyPlugin(config?: MyPluginConfig): DiagramsPlugin {
return {
name: "my-format",
version: "1.0.0",
apiVersion: "1.0",
runtimeSupport: {
node: true,
browser: true,
deno: true,
bun: true,
},
capabilities: [
// Importer capability
{
type: "importer",
name: "my-format",
extensions: [".my"],
mimeTypes: ["text/plain"],
canImport: async (source: string | string[]): Promise<boolean> => {
const sources = Array.isArray(source) ? source : [source];
return sources.every((s) => s.startsWith("MYFORMAT:"));
},
import: async (
source: string | string[],
diagram: Diagram,
context: ImportContext,
): Promise<void> => {
// Access diagrams-js exports via context.lib - NEVER import directly
const { Node } = context.lib;
const sources = Array.isArray(source) ? source : [source];
for (const src of sources) {
// Parse your format
const data = parseMyFormat(src);
// Create nodes
for (const item of data.items) {
const node = diagram.add(Node(item.name));
node.metadata = { source: item };
}
}
},
} as ImporterCapability,
// Exporter capability
{
type: "exporter",
name: "my-format",
extension: ".my",
mimeType: "text/plain",
export: async (diagram: Diagram, context: ExportContext): Promise<string> => {
const json = diagram.toJSON();
return formatAsMyFormat(json);
},
} as ExporterCapability,
],
};
}
// Pre-created instance for convenience
export const myPlugin = createMyPlugin();Package Configuration
json
{
"name": "@diagrams-js/plugin-my-format",
"version": "1.0.0",
"description": "My format plugin for diagrams-js",
"keywords": ["diagrams-js", "plugin"]
"type": "module",
"exports": {
".": "./dist/index.js",
"./package.json": "./package.json"
},
"files": ["dist", "LICENSE", "README.md", ".agents/skills"],
"peerDependencies": {
"diagrams-js": "^0.2.4"
},
"devDependencies": {
"diagrams-js": "file:../diagrams-js",
"typescript": "^6.0.2",
"vite-plus": "^0.1.14",
"vitest": "^3.0.0"
},
"scripts": {
"build": "vp pack src/index.ts && vp check --fix",
"test": "vp test",
"check": "vp check"
}
}Resource Discovery
Using context.loadResourcesList()
Access the resource discovery system to find provider icons:
ts
export function createMyPlugin(config?: MyPluginConfig): DiagramsPlugin {
let findResource: (query: string) => Array<{
provider: string;
type: string;
resource: string;
}>;
return {
name: "my-format",
version: "1.0.0",
apiVersion: "1.0",
runtimeSupport: { node: true, browser: true, deno: true, bun: true },
// Initialize plugin and load resource discovery
initialize: async (_config, context) => {
const module = await context.loadResourcesList();
if (module?.findResource) {
findResource = module.findResource;
}
},
capabilities: [
{
type: "importer",
name: "my-format",
extensions: [".my"],
mimeTypes: ["text/plain"],
import: async (source, diagram, context) => {
// Use findResource to map identifiers to provider icons
const matches = findResource("postgres");
// Returns: [{ provider: "onprem", type: "database", resource: "Postgresql" }, ...]
const json: DiagramJSON = {
name: "My Diagram",
nodes: [
{
id: "db",
label: "Database",
// Use discovered provider info
provider: matches[0]?.provider || "onprem",
service: matches[0]?.type || "database",
type: matches[0]?.resource || "Postgresql",
},
],
};
await diagram.import(JSON.stringify(json), "json");
},
} as ImporterCapability,
],
};
}YAML Parsing
Using context.loadYaml()
Plugins can parse and serialize YAML using context.loadYaml() without bundling their own YAML parser. This reduces plugin bundle size and ensures consistent parsing across all plugins:
ts
export function createMyPlugin(config?: MyPluginConfig): DiagramsPlugin {
return {
name: "my-yaml-format",
version: "1.0.0",
apiVersion: "1.0",
runtimeSupport: { node: true, browser: true, deno: true, bun: true },
capabilities: [
{
type: "importer",
name: "my-yaml-format",
extensions: [".yml", ".yaml"],
mimeTypes: ["text/yaml", "application/x-yaml"],
import: async (source, diagram, context) => {
const yaml = await context.loadYaml();
const data = yaml.load(source);
for (const item of data.items) {
const node = diagram.add(Node(item.name));
node.metadata = { source: item };
}
},
} as ImporterCapability,
{
type: "exporter",
name: "my-yaml-format",
extension: ".yml",
mimeType: "text/yaml",
export: async (diagram, context) => {
const yaml = await context.loadYaml();
const json = diagram.toJSON();
const data = {
version: "1.0",
services: json.nodes.map((n) => ({
name: n.label,
metadata: n.metadata,
})),
};
return yaml.dump(data);
},
} as ExporterCapability,
],
};
}- No need to add js-yaml or other YAML parsers to your plugin dependencies
- Reduces plugin bundle size
- Consistent YAML parsing across all plugins
- Lazy-loaded only when needed
- Works across all runtimes (browser, Node.js, Deno, Bun)
Plugin Types
Importer Plugin
ts
{
type: "importer",
name: "terraform",
extensions: [".tf", ".tfvars"],
mimeTypes: ["text/plain"],
canImport: async (source: string | string[]): Promise<boolean> => {
const sources = Array.isArray(source) ? source : [source];
return sources.every((s) => s.includes('resource "'));
},
import: async (source, diagram, context) => {
const { Node, Cluster } = context.lib;
const parsed = parseTerraform(source);
for (const resource of parsed.resources) {
const node = diagram.add(Node(resource.name));
node.metadata = { terraform: resource };
}
},
}Exporter Plugin
ts
{
type: "exporter",
name: "kubernetes",
extension: ".yaml",
mimeType: "text/yaml",
export: async (diagram, context) => {
const json = diagram.toJSON();
const manifests = json.nodes.map((node) => ({
apiVersion: "apps/v1",
kind: "Deployment",
metadata: { name: node.label },
spec: {
replicas: node.metadata?.replicas || 1,
template: {
spec: {
containers: [{ name: node.label, image: node.metadata?.image }],
},
},
},
}));
return YAML.stringify(manifests);
},
}Metadata Provider Plugin
ts
{
type: "metadata",
provider: "aws",
nodeTypes: ["EC2", "RDS", "Lambda"],
getMetadata: async (nodeType, nodeConfig, context) => {
// Fetch metadata from cloud provider API
return {
provider: "aws",
pricing: { hourly: 0.192, monthly: 140.16 },
specifications: { cpu: 4, memory: "16GB" },
};
},
}Hook Plugin
ts
{
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;
},
},
],
}Critical Patterns
ts
import type { Node, Edge, Diagram } from "diagrams-js"; // ✅ Type-only import
export const myPlugin = () => ({
import: async (source, diagram, context) => {
const { Node } = context.lib; // ✅ Access via context
const node = diagram.add(Node("name"));
},
});ts
let findResource: (query: string) => ResourceInfo[];
export const myPlugin = () => ({
initialize: async (_config, context) => {
const module = await context.loadResourcesList();
findResource = module.findResource;
},
import: async (source, diagram, context) => {
const matches = findResource("postgres"); // ✅ Dynamic lookup
const node: DiagramNodeJSON = {
id: "db",
label: "Database",
provider: matches[0]?.provider || "onprem",
service: matches[0]?.type || "database",
type: matches[0]?.resource || "Postgresql",
};
},
});ts
export const myPlugin = () => ({
import: async (source, diagram, context) => {
const yaml = await context.loadYaml(); // ✅ No bundling needed
const data = yaml.load(source);
// ...
},
});Publishing
- Build: npm run build
- Test: npm test
- Version: npm version patch|minor|major
- Publish: npm publish --access public
Naming Convention
Name your plugin with the diagrams-js-plugin- prefix so the CLI and users can find it:
json
{
"name": "diagrams-js-plugin-terraform"
}Or with a scope:
json
{
"name": "@myorg/diagrams-js-plugin-terraform"
}NPM Keywords for Discoverability
Add diagrams-js to your package.json keywords to make your plugin discoverable:
json
{
"name": "diagrams-js-plugin-my-format",
"keywords": ["diagrams-js", "diagrams", "architecture", "plugin", "import", "export"]
}Users can then find your plugin by searching npm for diagrams-js.
See Also
- diagrams-js-plugin-system - Plugin system overview and API
- plugin-docker-compose - Complete reference implementation
- diagrams-js documentation: https://diagrams-js.hatemhosny.dev