mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-31 22:23:27 +08:00
Merge pull request #268 from pangerlkr/patch-6
feat: add scripts/codemaps/generate.ts — fixes #247
This commit is contained in:
330
scripts/codemaps/generate.ts
Normal file
330
scripts/codemaps/generate.ts
Normal file
@@ -0,0 +1,330 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* scripts/codemaps/generate.ts
|
||||
*
|
||||
* Codemap Generator for everything-claude-code (ECC)
|
||||
*
|
||||
* Scans the current working directory and generates architectural
|
||||
* codemap documentation under docs/CODEMAPS/ as specified by the
|
||||
* doc-updater agent.
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx scripts/codemaps/generate.ts [srcDir]
|
||||
*
|
||||
* Output:
|
||||
* docs/CODEMAPS/INDEX.md
|
||||
* docs/CODEMAPS/frontend.md
|
||||
* docs/CODEMAPS/backend.md
|
||||
* docs/CODEMAPS/database.md
|
||||
* docs/CODEMAPS/integrations.md
|
||||
* docs/CODEMAPS/workers.md
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const SRC_DIR = process.argv[2] ? path.resolve(process.argv[2]) : ROOT;
|
||||
const OUTPUT_DIR = path.join(ROOT, 'docs', 'CODEMAPS');
|
||||
const TODAY = new Date().toISOString().split('T')[0];
|
||||
|
||||
// Patterns used to classify files into codemap areas
|
||||
const AREA_PATTERNS: Record<string, RegExp[]> = {
|
||||
frontend: [
|
||||
/\/(app|pages|components|hooks|contexts|ui|views|layouts|styles)\//i,
|
||||
/\.(tsx|jsx|css|scss|sass|less|vue|svelte)$/i,
|
||||
],
|
||||
backend: [
|
||||
/\/(api|routes|controllers|middleware|server|services|handlers)\//i,
|
||||
/\.(route|controller|handler|middleware|service)\.(ts|js)$/i,
|
||||
],
|
||||
database: [
|
||||
/\/(models|schemas|migrations|prisma|drizzle|db|database|repositories)\//i,
|
||||
/\.(model|schema|migration|seed)\.(ts|js)$/i,
|
||||
/prisma\/schema\.prisma$/,
|
||||
/schema\.sql$/,
|
||||
],
|
||||
integrations: [
|
||||
/\/(integrations?|third-party|external|plugins?|adapters?|connectors?)\//i,
|
||||
/\.(integration|adapter|connector)\.(ts|js)$/i,
|
||||
],
|
||||
workers: [
|
||||
/\/(workers?|jobs?|queues?|tasks?|cron|background)\//i,
|
||||
/\.(worker|job|queue|task|cron)\.(ts|js)$/i,
|
||||
],
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// File System Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Recursively collect all files under a directory, skipping common noise dirs. */
|
||||
function walkDir(dir: string, results: string[] = []): string[] {
|
||||
const SKIP = new Set([
|
||||
'node_modules', '.git', '.next', '.nuxt', 'dist', 'build', 'out',
|
||||
'.turbo', 'coverage', '.cache', '__pycache__', '.venv', 'venv',
|
||||
]);
|
||||
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return results;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (SKIP.has(entry.name)) continue;
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walkDir(fullPath, results);
|
||||
} else if (entry.isFile()) {
|
||||
results.push(fullPath);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/** Return path relative to ROOT, always using forward slashes. */
|
||||
function rel(p: string): string {
|
||||
return path.relative(ROOT, p).replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Analysis
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface AreaInfo {
|
||||
name: string;
|
||||
files: string[];
|
||||
entryPoints: string[];
|
||||
directories: string[];
|
||||
}
|
||||
|
||||
function classifyFiles(allFiles: string[]): Record<string, AreaInfo> {
|
||||
const areas: Record<string, AreaInfo> = {
|
||||
frontend: { name: 'Frontend', files: [], entryPoints: [], directories: [] },
|
||||
backend: { name: 'Backend/API', files: [], entryPoints: [], directories: [] },
|
||||
database: { name: 'Database', files: [], entryPoints: [], directories: [] },
|
||||
integrations: { name: 'Integrations', files: [], entryPoints: [], directories: [] },
|
||||
workers: { name: 'Workers', files: [], entryPoints: [], directories: [] },
|
||||
};
|
||||
|
||||
for (const file of allFiles) {
|
||||
const relPath = rel(file);
|
||||
for (const [area, patterns] of Object.entries(AREA_PATTERNS)) {
|
||||
if (patterns.some((p) => p.test(relPath))) {
|
||||
areas[area].files.push(relPath);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Derive unique directories and entry points per area
|
||||
for (const area of Object.values(areas)) {
|
||||
const dirs = new Set(area.files.map((f) => path.dirname(f)));
|
||||
area.directories = [...dirs].sort();
|
||||
|
||||
area.entryPoints = area.files
|
||||
.filter((f) => /index\.(ts|tsx|js|jsx)$/.test(f) || /main\.(ts|tsx|js|jsx)$/.test(f))
|
||||
.slice(0, 10);
|
||||
}
|
||||
|
||||
return areas;
|
||||
}
|
||||
|
||||
/** Count lines in a file (returns 0 on error). */
|
||||
function lineCount(p: string): number {
|
||||
try {
|
||||
const content = fs.readFileSync(p, 'utf8');
|
||||
return content.split('\n').length;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** Build a simple directory tree ASCII diagram (max 3 levels deep). */
|
||||
function buildTree(dir: string, prefix = '', depth = 0): string {
|
||||
if (depth > 2) return '';
|
||||
const SKIP = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage']);
|
||||
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
|
||||
const dirs = entries.filter((e) => e.isDirectory() && !SKIP.has(e.name));
|
||||
const files = entries.filter((e) => e.isFile());
|
||||
|
||||
let result = '';
|
||||
const items = [...dirs, ...files];
|
||||
items.forEach((entry, i) => {
|
||||
const isLast = i === items.length - 1;
|
||||
const connector = isLast ? '└── ' : '├── ';
|
||||
result += `${prefix}${connector}${entry.name}\n`;
|
||||
if (entry.isDirectory()) {
|
||||
const newPrefix = prefix + (isLast ? ' ' : '│ ');
|
||||
result += buildTree(path.join(dir, entry.name), newPrefix, depth + 1);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Markdown Generators
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function generateAreaDoc(areaKey: string, area: AreaInfo, allFiles: string[]): string {
|
||||
const fileCount = area.files.length;
|
||||
const totalLines = area.files.reduce((sum, f) => sum + lineCount(path.join(ROOT, f)), 0);
|
||||
|
||||
const entrySection = area.entryPoints.length > 0
|
||||
? area.entryPoints.map((e) => `- \`${e}\``).join('\n')
|
||||
: '- *(no index/main entry points detected)*';
|
||||
|
||||
const dirSection = area.directories.slice(0, 20)
|
||||
.map((d) => `- \`${d}/\``)
|
||||
.join('\n') || '- *(no dedicated directories detected)*';
|
||||
|
||||
const fileSection = area.files.slice(0, 30)
|
||||
.map((f) => `| \`${f}\` | ${lineCount(path.join(ROOT, f))} |`)
|
||||
.join('\n');
|
||||
|
||||
const moreFiles = area.files.length > 30
|
||||
? `\n*...and ${area.files.length - 30} more files*`
|
||||
: '';
|
||||
|
||||
return `# ${area.name} Codemap
|
||||
|
||||
**Last Updated:** ${TODAY}
|
||||
**Total Files:** ${fileCount}
|
||||
**Total Lines:** ${totalLines}
|
||||
|
||||
## Entry Points
|
||||
|
||||
${entrySection}
|
||||
|
||||
## Architecture
|
||||
|
||||
\`\`\`
|
||||
${area.name} Directory Structure
|
||||
${dirSection.replace(/- `/g, '').replace(/`\/$/gm, '/')}
|
||||
\`\`\`
|
||||
|
||||
## Key Modules
|
||||
|
||||
| File | Lines |
|
||||
|------|-------|
|
||||
${fileSection}${moreFiles}
|
||||
|
||||
## Data Flow
|
||||
|
||||
> Detected from file patterns. Review individual files for detailed data flow.
|
||||
|
||||
## External Dependencies
|
||||
|
||||
> Run \`npx jsdoc2md src/**/*.ts\` to extract JSDoc and identify external dependencies.
|
||||
|
||||
## Related Areas
|
||||
|
||||
- [INDEX](./INDEX.md) — Full overview
|
||||
- [Frontend](./frontend.md)
|
||||
- [Backend/API](./backend.md)
|
||||
- [Database](./database.md)
|
||||
- [Integrations](./integrations.md)
|
||||
- [Workers](./workers.md)
|
||||
`;
|
||||
}
|
||||
|
||||
function generateIndex(areas: Record<string, AreaInfo>, allFiles: string[]): string {
|
||||
const totalFiles = allFiles.length;
|
||||
const areaRows = Object.entries(areas)
|
||||
.map(([key, area]) => `| [${area.name}](./${key}.md) | ${area.files.length} files | ${area.directories.slice(0, 3).map((d) => `\`${d}\``).join(', ') || '—'} |`)
|
||||
.join('\n');
|
||||
|
||||
const topLevelTree = buildTree(SRC_DIR);
|
||||
|
||||
return `# Codebase Overview — CODEMAPS Index
|
||||
|
||||
**Last Updated:** ${TODAY}
|
||||
**Root:** \`${rel(SRC_DIR) || '.'}\`
|
||||
**Total Files Scanned:** ${totalFiles}
|
||||
|
||||
## Areas
|
||||
|
||||
| Area | Size | Key Directories |
|
||||
|------|------|-----------------|
|
||||
${areaRows}
|
||||
|
||||
## Repository Structure
|
||||
|
||||
\`\`\`
|
||||
${rel(SRC_DIR) || path.basename(SRC_DIR)}/
|
||||
${topLevelTree}\`\`\`
|
||||
|
||||
## How to Regenerate
|
||||
|
||||
\`\`\`bash
|
||||
npx tsx scripts/codemaps/generate.ts # Regenerate codemaps
|
||||
npx madge --image graph.svg src/ # Dependency graph (requires graphviz)
|
||||
npx jsdoc2md src/**/*.ts # Extract JSDoc
|
||||
\`\`\`
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Frontend](./frontend.md) — UI components, pages, hooks
|
||||
- [Backend/API](./backend.md) — API routes, controllers, middleware
|
||||
- [Database](./database.md) — Models, schemas, migrations
|
||||
- [Integrations](./integrations.md) — External services & adapters
|
||||
- [Workers](./workers.md) — Background jobs, queues, cron tasks
|
||||
`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function main(): void {
|
||||
console.log(`[generate.ts] Scanning: ${SRC_DIR}`);
|
||||
console.log(`[generate.ts] Output: ${OUTPUT_DIR}`);
|
||||
|
||||
// Ensure output directory exists
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
|
||||
// Walk the directory tree
|
||||
const allFiles = walkDir(SRC_DIR);
|
||||
console.log(`[generate.ts] Found ${allFiles.length} files`);
|
||||
|
||||
// Classify files into areas
|
||||
const areas = classifyFiles(allFiles);
|
||||
|
||||
// Generate INDEX.md
|
||||
const indexContent = generateIndex(areas, allFiles);
|
||||
const indexPath = path.join(OUTPUT_DIR, 'INDEX.md');
|
||||
fs.writeFileSync(indexPath, indexContent, 'utf8');
|
||||
console.log(`[generate.ts] Written: ${rel(indexPath)}`);
|
||||
|
||||
// Generate per-area codemaps
|
||||
for (const [key, area] of Object.entries(areas)) {
|
||||
const content = generateAreaDoc(key, area, allFiles);
|
||||
const outPath = path.join(OUTPUT_DIR, `${key}.md`);
|
||||
fs.writeFileSync(outPath, content, 'utf8');
|
||||
console.log(`[generate.ts] Written: ${rel(outPath)} (${area.files.length} files)`);
|
||||
}
|
||||
|
||||
console.log('\n[generate.ts] Done! Codemaps written to docs/CODEMAPS/');
|
||||
console.log('[generate.ts] Files generated:');
|
||||
console.log(' docs/CODEMAPS/INDEX.md');
|
||||
console.log(' docs/CODEMAPS/frontend.md');
|
||||
console.log(' docs/CODEMAPS/backend.md');
|
||||
console.log(' docs/CODEMAPS/database.md');
|
||||
console.log(' docs/CODEMAPS/integrations.md');
|
||||
console.log(' docs/CODEMAPS/workers.md');
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user