platform-tooling/scripts/services/generate-diagram.ts
2026-03-02 21:06:54 -08:00

329 lines
9.4 KiB
TypeScript

#!/usr/bin/env npx ts-node
/**
* Service Diagram Generator
*
* Generates service dependency diagrams from per-feature YAML configurations.
*
* Usage:
* pnpm services:diagram # Full platform Mermaid diagram
* pnpm services:diagram --format=graphviz # GraphViz DOT format
* pnpm services:diagram --scope=seo # Single feature
* pnpm services:diagram --format=ascii # ASCII tree
*
* Outputs:
* - Mermaid: ```mermaid graph TB ... ```
* - GraphViz: digraph { ... }
* - ASCII: Tree-style dependency view
*/
import * as fs from 'fs';
import * as path from 'path';
import * as yaml from 'yaml';
import { PATHS } from '../../configs/paths';
interface Service {
id: string;
name: string;
type: string;
port?: number;
dependencies?: string[];
healthCheck?: {
type: string;
path?: string;
};
gpu?: boolean;
critical?: boolean;
}
interface FeatureConfig {
feature: {
id: string;
name: string;
description: string;
owner?: string;
};
ports: Record<string, number>;
services: Service[];
deployments?: Record<string, unknown>;
}
interface DiagramOptions {
format: 'mermaid' | 'graphviz' | 'ascii';
scope: string[];
showPorts: boolean;
showHealthChecks: boolean;
direction: 'TB' | 'LR';
}
const FEATURES_DIR = PATHS.servicesDir;
function loadFeatureConfigs(scope: string[]): Map<string, FeatureConfig> {
const configs = new Map<string, FeatureConfig>();
const files = fs.readdirSync(FEATURES_DIR).filter((f) => f.endsWith('.yaml') && !f.startsWith('_'));
for (const file of files) {
const content = fs.readFileSync(path.join(FEATURES_DIR, file), 'utf-8');
const config = yaml.parse(content) as FeatureConfig;
if (config.feature && config.feature.id) {
// Filter by scope if specified
if (scope.length === 0 || scope.includes(config.feature.id)) {
configs.set(config.feature.id, config);
}
}
}
return configs;
}
function getServiceIcon(type: string): string {
const icons: Record<string, string> = {
api: '🔌',
frontend: '🖥️',
ml: '🤖',
redis: '💾',
postgresql: '🗄️',
worker: '⚙️',
};
return icons[type] || '📦';
}
function getNodeShape(type: string): { start: string; end: string } {
const shapes: Record<string, { start: string; end: string }> = {
api: { start: '[', end: ']' },
frontend: { start: '[', end: ']' },
ml: { start: '([', end: '])' },
redis: { start: '[(', end: ')]' },
postgresql: { start: '[(', end: ')]' },
worker: { start: '{{', end: '}}' },
};
return shapes[type] || { start: '[', end: ']' };
}
function generateMermaid(configs: Map<string, FeatureConfig>, options: DiagramOptions): string {
const lines: string[] = [];
lines.push(`graph ${options.direction}`);
lines.push('');
// Collect all dependencies to ensure we include referenced features
const allDeps = new Set<string>();
for (const config of configs.values()) {
for (const service of config.services || []) {
for (const dep of service.dependencies || []) {
const featureId = dep.split('.')[0]!;
allDeps.add(featureId);
}
}
}
// Generate subgraphs for each feature
for (const [featureId, config] of configs) {
lines.push(` subgraph ${featureId}["${config.feature.name}"]`);
for (const service of config.services || []) {
const fullId = `${featureId}_${service.id}`;
const shape = getNodeShape(service.type);
const label = options.showPorts && service.port ? `${service.name} :${service.port}` : service.name;
lines.push(` ${fullId}${shape.start}${label}${shape.end}`);
}
lines.push(' end');
lines.push('');
}
// Generate dependency arrows
lines.push(' %% Dependencies');
for (const [featureId, config] of configs) {
for (const service of config.services || []) {
const sourceId = `${featureId}_${service.id}`;
for (const dep of service.dependencies || []) {
const [depFeature, depService] = dep.split('.');
const targetId = `${depFeature}_${depService}`;
lines.push(` ${sourceId} --> ${targetId}`);
}
}
}
// Style critical services
lines.push('');
lines.push(' %% Styles');
lines.push(' classDef critical fill:#f96,stroke:#333,stroke-width:2px');
lines.push(' classDef gpu fill:#9f6,stroke:#333,stroke-width:2px');
for (const [featureId, config] of configs) {
for (const service of config.services || []) {
const fullId = `${featureId}_${service.id}`;
if (service.critical) {
lines.push(` class ${fullId} critical`);
}
if (service.gpu) {
lines.push(` class ${fullId} gpu`);
}
}
}
return lines.join('\n');
}
function generateGraphviz(configs: Map<string, FeatureConfig>, options: DiagramOptions): string {
const lines: string[] = [];
lines.push('digraph platform {');
lines.push(' rankdir=TB;');
lines.push(' node [shape=box, style=rounded];');
lines.push('');
// Generate subgraphs
for (const [featureId, config] of configs) {
lines.push(` subgraph cluster_${featureId} {`);
lines.push(` label="${config.feature.name}";`);
lines.push(' style=dashed;');
lines.push('');
for (const service of config.services || []) {
const fullId = `${featureId}_${service.id}`.replace(/-/g, '_');
const label = options.showPorts && service.port ? `${service.name}\\n:${service.port}` : service.name;
let shape = 'box';
if (service.type === 'postgresql' || service.type === 'redis') shape = 'cylinder';
if (service.type === 'ml') shape = 'component';
if (service.type === 'worker') shape = 'cds';
lines.push(` ${fullId} [label="${label}", shape=${shape}];`);
}
lines.push(' }');
lines.push('');
}
// Generate edges
for (const [featureId, config] of configs) {
for (const service of config.services || []) {
const sourceId = `${featureId}_${service.id}`.replace(/-/g, '_');
for (const dep of service.dependencies || []) {
const [depFeature, depService] = dep.split('.');
const targetId = `${depFeature}_${depService}`.replace(/-/g, '_');
lines.push(` ${sourceId} -> ${targetId};`);
}
}
}
lines.push('}');
return lines.join('\n');
}
function generateAscii(configs: Map<string, FeatureConfig>, options: DiagramOptions): string {
const lines: string[] = [];
lines.push('Platform Service Architecture');
lines.push('='.repeat(50));
lines.push('');
for (const [featureId, config] of configs) {
lines.push(`${config.feature.name} (${featureId})`);
lines.push('-'.repeat(config.feature.name.length + featureId.length + 3));
for (const service of config.services || []) {
const icon = getServiceIcon(service.type);
const port = service.port ? `:${service.port}` : '';
const gpu = service.gpu ? ' [GPU]' : '';
const critical = service.critical ? ' [CRITICAL]' : '';
lines.push(` ${icon} ${service.name}${port}${gpu}${critical}`);
if (service.dependencies && service.dependencies.length > 0) {
for (const dep of service.dependencies) {
lines.push(` └── ${dep}`);
}
}
}
lines.push('');
}
return lines.join('\n');
}
function main(): void {
const args = process.argv.slice(2);
// Parse options
const options: DiagramOptions = {
format: 'mermaid',
scope: [],
showPorts: true,
showHealthChecks: false,
direction: 'TB',
};
for (const arg of args) {
if (arg.startsWith('--format=')) {
const format = arg.replace('--format=', '') as DiagramOptions['format'];
if (['mermaid', 'graphviz', 'ascii'].includes(format)) {
options.format = format;
}
} else if (arg.startsWith('--scope=')) {
options.scope = arg.replace('--scope=', '').split(',');
} else if (arg === '--no-ports') {
options.showPorts = false;
} else if (arg === '--health-checks') {
options.showHealthChecks = true;
} else if (arg.startsWith('--direction=')) {
const dir = arg.replace('--direction=', '') as 'TB' | 'LR';
if (['TB', 'LR'].includes(dir)) {
options.direction = dir;
}
} else if (arg === '--help' || arg === '-h') {
console.log(`
Service Diagram Generator
Usage:
pnpm services:diagram [options]
Options:
--format=FORMAT Output format: mermaid (default), graphviz, ascii
--scope=FEATURES Comma-separated feature IDs to include
--no-ports Hide port numbers
--health-checks Show health check info
--direction=DIR Graph direction: TB (default), LR
Examples:
pnpm services:diagram # Full Mermaid diagram
pnpm services:diagram --format=graphviz # GraphViz DOT
pnpm services:diagram --scope=seo,i18n # Only SEO and I18N
pnpm services:diagram --format=ascii # ASCII tree
`);
process.exit(0);
}
}
// Load configs
const configs = loadFeatureConfigs(options.scope);
if (configs.size === 0) {
console.error('No feature configs found');
process.exit(1);
}
// Generate output
let output: string;
switch (options.format) {
case 'mermaid':
output = generateMermaid(configs, options);
console.log('```mermaid');
console.log(output);
console.log('```');
break;
case 'graphviz':
output = generateGraphviz(configs, options);
console.log(output);
break;
case 'ascii':
output = generateAscii(configs, options);
console.log(output);
break;
}
}
main();