#!/usr/bin/env node /** * generate-dependency-graph.js * * Generates two files in docs/ on every commit: * * docs/dependency-graph.json — full import graph for src/lib/game/ * docs/circular-deps.txt — list of circular dependency chains (empty = clean) * * Run manually: node .husky/scripts/generate-dependency-graph.js * Requires: bun add -d madge */ const path = require('path'); const fs = require('fs'); const { execSync } = require('child_process'); const ROOT = path.resolve(__dirname, '../../'); const DOCS_DIR = path.join(ROOT, 'docs'); const GRAPH_OUT = path.join(DOCS_DIR, 'dependency-graph.json'); const CIRCULAR_OUT = path.join(DOCS_DIR, 'circular-deps.txt'); // Check madge is available function madgeAvailable() { try { execSync('bunx madge --version', { stdio: 'ignore', cwd: ROOT }); return true; } catch { return false; } } function run(cmd) { return execSync(cmd, { cwd: ROOT, encoding: 'utf8' }); } if (!madgeAvailable()) { console.error('madge not found. Install with: bun add -d madge'); process.exit(1); } if (!fs.existsSync(DOCS_DIR)) { fs.mkdirSync(DOCS_DIR, { recursive: true }); } // ── 1. Full dependency graph for the game library ───────────────────────── try { const graphJson = run( 'bunx madge --json --extensions ts,tsx --exclude "\\.test\\.|__tests__" src/lib/game' ); // Parse and re-serialize with readable formatting const graph = JSON.parse(graphJson); // Annotate with metadata for AI agents const output = { _meta: { generated: new Date().toISOString(), description: 'Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.', usage: 'To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry.', }, graph, }; fs.writeFileSync(GRAPH_OUT, JSON.stringify(output, null, 2)); const nodeCount = Object.keys(graph).length; console.log(`✅ Dependency graph: ${nodeCount} modules → docs/dependency-graph.json`); } catch (err) { console.error('Failed to generate dependency graph:', err.message); process.exit(1); } // ── 2. Circular dependency report ───────────────────────────────────────── try { let circularOutput = ''; try { // madge exits with code 1 when circulars are found; capture stdout anyway circularOutput = run( 'bunx madge --circular --extensions ts,tsx --exclude "\\.test\\.|__tests__" src/lib/game' ); } catch (e) { // exitCode 1 = circulars found; stdout contains the list circularOutput = e.stdout || ''; } const lines = circularOutput.trim().split('\n').filter(Boolean); // madge circular output starts with "Found N circular dependencies!" const circularLines = lines.filter( (l) => !l.startsWith('Found') && !l.startsWith('✔') && l.trim() ); let content; if (circularLines.length === 0) { content = `# Circular Dependencies\nGenerated: ${new Date().toISOString()}\n\nNo circular dependencies found. ✅\n`; console.log('✅ No circular dependencies found'); } else { content = [ `# Circular Dependencies`, `Generated: ${new Date().toISOString()}`, `Found: ${circularLines.length} circular chain(s) — these MUST be fixed before modifying involved files.`, '', ...circularLines.map((l, i) => `${i + 1}. ${l.trim()}`), '', '## How to fix', '1. Identify which import in the chain can be extracted to a shared types/utils file.', '2. Move the shared type or function there.', '3. Both files import from the new shared module instead of each other.', '4. Run: bunx madge --circular src/lib/game (should return clean)', ].join('\n'); console.warn(`⚠️ Found ${circularLines.length} circular dependency chain(s) — see docs/circular-deps.txt`); } fs.writeFileSync(CIRCULAR_OUT, content); } catch (err) { console.error('Failed to check circular dependencies:', err.message); // Non-fatal: write a note to the file and continue fs.writeFileSync(CIRCULAR_OUT, `# Circular Dependencies\nError running check: ${err.message}\n`); }