import * as fs from 'fs'; import * as cron from 'node-cron'; import { FigmaClient } from './figma-client'; import { RuleEngine } from './rules/rule-engine'; import { HTMLReporter } from './reporters/html-reporter'; import { SlackNotifier } from './notifiers/slack-notifier'; import { DiscordNotifier } from './notifiers/discord-notifier'; import { GitHubNotifier } from './notifiers/github-notifier'; import { BotConfig, DesignRules, Violation, ComplianceReport } from './types'; export class DesignComplianceBot { private figmaClient: FigmaClient; private rules: DesignRules; private ruleEngine: RuleEngine; private slackNotifier?: SlackNotifier; private discordNotifier?: DiscordNotifier; private githubNotifier?: GitHubNotifier; constructor(config: BotConfig) { this.figmaClient = new FigmaClient(config.figmaToken); // Load rules if (typeof config.rules === 'string') { this.rules = JSON.parse(fs.readFileSync(config.rules, 'utf-8')); } else { this.rules = config.rules; } this.ruleEngine = new RuleEngine(this.rules, this.figmaClient); // Initialize notifiers if (config.slackWebhook || this.rules.notifications?.slack?.webhook) { this.slackNotifier = new SlackNotifier( config.slackWebhook || this.rules.notifications!.slack!.webhook! ); } if (config.discordWebhook || this.rules.notifications?.discord?.webhook) { this.discordNotifier = new DiscordNotifier( config.discordWebhook || this.rules.notifications!.discord!.webhook! ); } if (config.githubToken && this.rules.github?.enabled) { this.githubNotifier = new GitHubNotifier( config.githubToken, this.rules.github.repo!, this.rules.github.labels || [] ); } } async check(fileKey: string): Promise { const file = await this.figmaClient.getFile(fileKey); const violations: Violation[] = []; // Traverse all pages and frames for (const page of file.document.children || []) { const pageNodes = this.figmaClient.extractAllNodes(page); for (const node of pageNodes) { const nodeViolations = this.ruleEngine.checkNode( node, page.name, node.type === 'FRAME' ? node.name : undefined ); violations.push(...nodeViolations); } } // Send notifications if (violations.length > 0) { const report = this.createReport(fileKey, file.name, violations); if (this.slackNotifier) { await this.slackNotifier.notify(report); } if (this.discordNotifier) { await this.discordNotifier.notify(report); } if (this.githubNotifier) { await this.githubNotifier.createIssues(report); } } return violations; } async generateReport( fileKey: string, options: { format: 'html' | 'pdf'; output: string } ): Promise { const violations = await this.check(fileKey); const file = await this.figmaClient.getFile(fileKey); const report = this.createReport(fileKey, file.name, violations); const reporter = new HTMLReporter(); const html = await reporter.generate(report); if (options.format === 'html') { fs.writeFileSync(options.output, html); return options.output; } else { // PDF generation const pdf = require('html-pdf-node'); const pdfBuffer = await pdf.generatePdf({ content: html }, { format: 'A4' }); fs.writeFileSync(options.output, pdfBuffer); return options.output; } } schedule(cronExpression: string, callback: () => Promise): void { cron.schedule(cronExpression, async () => { try { await callback(); } catch (error) { console.error('Scheduled check failed:', error); } }); } addRule(name: string, rule: { validate: (node: any) => boolean; message: string }): void { // Custom rule support - extend RuleEngine console.log(`Custom rule "${name}" registered`); } private createReport( fileKey: string, fileName: string, violations: Violation[] ): ComplianceReport { const summary = { total: violations.length, bySeverity: {} as Record, byType: {} as Record, }; for (const v of violations) { summary.bySeverity[v.severity] = (summary.bySeverity[v.severity] || 0) + 1; summary.byType[v.type] = (summary.byType[v.type] || 0) + 1; } return { fileKey, fileName, timestamp: new Date().toISOString(), violations, summary, }; } } export * from './types';