diagrams-js
Version:
A TypeScript port of the diagrams Python library for drawing cloud system architecture diagrams as code
482 lines (400 loc) • 12.4 kB
Markdown
---
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
1. **Never import from `diagrams-js`** - Access runtime exports via `context.lib` to avoid multiple library instances. Type imports are allowed
2. **Use `context.loadResourcesList()`** - Dynamically discover provider icons and resources
3. **Use `context.loadYaml()`** - Parse and serialize YAML without bundling dependencies
4. **Convert to/from JSON** - Use diagrams-js JSON format as the intermediary for imports
5. **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
```typescript
// 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:
```typescript
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:
```typescript
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
```typescript
{
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
```typescript
{
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
```typescript
{
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
```typescript
{
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
```typescript
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"));
},
});
```
```typescript
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",
};
},
});
```
```typescript
export const myPlugin = () => ({
import: async (source, diagram, context) => {
const yaml = await context.loadYaml(); // ✅ No bundling needed
const data = yaml.load(source);
// ...
},
});
```
## Publishing
1. **Build**: `npm run build`
2. **Test**: `npm test`
3. **Version**: `npm version patch|minor|major`
4. **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