| name | diagrams-js/creating-plugins |
| description | Create custom plugins for diagrams-js to extend import/export capabilities. Package structure, plugin API, best practices, and real-world examples. Use context.lib for runtime exports, context.loadResourcesList for resource discovery, and context.loadYaml for YAML parsing without bundling dependencies. |
| type | feature |
| library | diagrams-js |
| library_version | 0.5.0 |
| sources | ["hatemhosny/diagrams-js:src/plugins/types.ts","hatemhosny/plugin-docker-compose:src/index.ts"] |
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
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 documentation
Quick Start
Minimal Plugin Package
import type {
DiagramsPlugin,
ImporterCapability,
ExporterCapability,
Diagram,
ImportContext,
ExportContext,
} from "diagrams-js";
export interface MyPluginConfig {
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: [
{
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> => {
const { Node } = context.lib;
const sources = Array.isArray(source) ? source : [source];
for (const src of sources) {
const data = parseMyFormat(src);
for (const item of data.items) {
const node = diagram.add(Node(item.name));
node.metadata = { source: item };
}
}
},
} as ImporterCapability,
{
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,
],
};
}
export const myPlugin = createMyPlugin();
Package Configuration
{
"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:
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: 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) => {
const matches = findResource("postgres");
const json: DiagramJSON = {
name: "My Diagram",
nodes: [
{
id: "db",
label: "Database",
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:
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
{
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
{
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
{
type: "metadata",
provider: "aws",
nodeTypes: ["EC2", "RDS", "Lambda"],
getMetadata: async (nodeType, nodeConfig, context) => {
return {
provider: "aws",
pricing: { hourly: 0.192, monthly: 140.16 },
specifications: { cpu: 4, memory: "16GB" },
};
},
}
Hook Plugin
{
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
import type { Node, Edge, Diagram } from "diagrams-js";
export const myPlugin = () => ({
import: async (source, diagram, context) => {
const { Node } = context.lib;
const node = diagram.add(Node("name"));
},
});
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");
const node: DiagramNodeJSON = {
id: "db",
label: "Database",
provider: matches[0]?.provider || "onprem",
service: matches[0]?.type || "database",
type: matches[0]?.resource || "Postgresql",
};
},
});
export const myPlugin = () => ({
import: async (source, diagram, context) => {
const yaml = await context.loadYaml();
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:
{
"name": "diagrams-js-plugin-terraform"
}
Or with a scope:
{
"name": "@myorg/diagrams-js-plugin-terraform"
}
NPM Keywords for Discoverability
Add diagrams-js to your package.json keywords to make your plugin discoverable:
{
"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