Skip to content

Writing Generators

Extend Archlette. Transform IR to custom DSL formats.


Generator Interface

All generators export a default function:

export default function myGenerator(ir: ArchletteIR, node: ResolvedStageNode): string {
  // Transform IR to DSL
  return 'workspace { ... }';
}

Input: ArchletteIR — Validated architecture, ResolvedStageNode — Configuration

Output: String containing DSL content


Example: PlantUML Generator

import type { ArchletteIR, ResolvedStageNode } from '@chrislyons-dev/archlette';

export default function plantumlGenerator(
  ir: ArchletteIR,
  node: ResolvedStageNode,
): string {
  const lines: string[] = [];

  lines.push('@startuml');
  lines.push(`title ${ir.system.name} - Component Diagram`);
  lines.push('');

  // Generate components
  for (const component of ir.components) {
    lines.push(`component [${component.name}] as ${component.id}`);
  }

  lines.push('');

  // Generate relationships
  for (const rel of ir.componentRelationships) {
    const desc = rel.description ? ` : ${rel.description}` : '';
    lines.push(`${rel.source} --> ${rel.destination}${desc}`);
  }

  lines.push('@enduml');

  return lines.join('\n');
}

Example: Mermaid Generator

import type { ArchletteIR, ResolvedStageNode } from '@chrislyons-dev/archlette';

export default function mermaidGenerator(
  ir: ArchletteIR,
  node: ResolvedStageNode,
): string {
  const lines: string[] = [];

  lines.push('graph TD');
  lines.push(`  %% ${ir.system.name}`);
  lines.push('');

  // Generate components
  for (const component of ir.components) {
    const label = component.description
      ? `${component.name}<br/>${component.description}`
      : component.name;
    lines.push(`  ${component.id}[${label}]`);
  }

  lines.push('');

  // Generate relationships
  for (const rel of ir.componentRelationships) {
    const label = rel.description ? `|${rel.description}|` : '';
    lines.push(`  ${rel.source} --> ${label} ${rel.destination}`);
  }

  return lines.join('\n');
}

Example: GraphViz DOT Generator

import type { ArchletteIR, ResolvedStageNode } from '@chrislyons-dev/archlette';

export default function dotGenerator(ir: ArchletteIR, node: ResolvedStageNode): string {
  const lines: string[] = [];

  lines.push('digraph architecture {');
  lines.push('  rankdir=LR;');
  lines.push('  node [shape=box, style=rounded];');
  lines.push('');

  // Generate components
  for (const component of ir.components) {
    const label = component.description
      ? `${component.name}\\n${component.description}`
      : component.name;
    lines.push(`  ${component.id} [label="${label}"];`);
  }

  lines.push('');

  // Generate relationships
  for (const rel of ir.componentRelationships) {
    const label = rel.description ? `label="${rel.description}"` : '';
    lines.push(`  ${rel.source} -> ${rel.destination} [${label}];`);
  }

  lines.push('}');

  return lines.join('\n');
}

Using Custom Generators

Reference in config:

generators:
  - use: generators/builtin/structurizr # Structurizr DSL
  - use: ./custom/plantuml-generator # PlantUML
  - use: ./custom/mermaid-generator # Mermaid

Multiple generators can run. Each produces separate output file.


Template Engines

Use Nunjucks for complex DSL generation:

import nunjucks from 'nunjucks';
import type { ArchletteIR, ResolvedStageNode } from '@chrislyons-dev/archlette';

export default function templateGenerator(
  ir: ArchletteIR,
  node: ResolvedStageNode,
): string {
  const template = `
workspace "{{ system.name }}" {
  {% for component in components %}
  component "{{ component.name }}" {
    description "{{ component.description }}"
  }
  {% endfor %}
}
  `.trim();

  return nunjucks.renderString(template, ir);
}

Built-in Structurizr generator uses this approach. See src/generators/builtin/structurizr.ts.


Multi-File Generators

Return object for multiple files:

import type {
  ArchletteIR,
  ResolvedStageNode,
  GeneratorOutput,
} from '@chrislyons-dev/archlette';

export default function multiFileGenerator(
  ir: ArchletteIR,
  node: ResolvedStageNode,
): GeneratorOutput {
  return {
    files: [
      {
        path: 'workspace.dsl',
        content: generateWorkspace(ir),
      },
      {
        path: 'views.dsl',
        content: generateViews(ir),
      },
    ],
  };
}

Best Practices

Escape special characters — Prevent DSL syntax errors.

Use stable output — Same IR, same DSL.

Add comments — Help users understand generated DSL.

Validate IR — Check for required fields before generation.

Handle edge cases — Empty descriptions, missing relationships.

Test with real data — Use Archlette's own IR for testing.


Common Patterns

Escaping strings:

function escape(str: string): string {
  return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
}

Conditional output:

const description = component.description
  ? `description "${escape(component.description)}"`
  : '';

Filtering empty arrays:

if (ir.components.length === 0) {
  console.warn('No components to generate');
  return '// No components found';
}

Grouping by container:

const byContainer = new Map<string, Component[]>();
for (const component of ir.components) {
  if (!byContainer.has(component.containerId)) {
    byContainer.set(component.containerId, []);
  }
  byContainer.get(component.containerId)!.push(component);
}

Output Configuration

Generators write to path configured in .aac.yaml:

paths:
  dsl_out: docs/architecture/workspace.dsl  # Single file
  # or
  dsl_out: docs/architecture/dsl  # Directory for multi-file

Generator doesn't handle file I/O. Returns string. Pipeline writes to disk.