329 lines
9.4 KiB
TypeScript
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();
|