The doc-sniffing dog
at main 11 kB view raw
1import { bold, cyan, gray, green, red, yellow } from "@std/fmt/colors"; 2import { join, resolve } from "@std/path"; 3import { analyzeDirectory } from "./core.ts"; 4 5interface WorkspaceConfig { 6 workspace?: string[]; 7 imports?: Record<string, string>; 8} 9 10interface WorkspaceMemberStats { 11 name: string; 12 path: string; 13 hasDenoJson: boolean; 14 hasExports: boolean; 15 exportPath?: string; 16 stats: { 17 total: number; 18 documented: number; 19 undocumented: number; 20 percentage: number; 21 }; 22 byType?: Record<string, { total: number; documented: number }>; 23} 24 25interface WorkspaceReport { 26 members: WorkspaceMemberStats[]; 27 aggregate: { 28 totalMembers: number; 29 totalExports: number; 30 totalDocumented: number; 31 totalUndocumented: number; 32 averagePercentage: number; 33 }; 34} 35 36/** 37 * Analyzer for Deno workspace configurations. 38 * 39 * The WorkspaceAnalyzer helps Doggo analyze multi-package repositories 40 * by examining each workspace member individually and providing both 41 * individual and aggregate documentation coverage statistics. 42 * 43 * @example 44 * ```typescript 45 * import { WorkspaceAnalyzer } from "@doggo/cli"; 46 * 47 * const analyzer = new WorkspaceAnalyzer("./my-workspace"); 48 * const report = await analyzer.analyze(); 49 * 50 * if (report) { 51 * console.log(`Total coverage: ${report.aggregate.averagePercentage}%`); 52 * console.log(`Pack members: ${report.members.length}`); 53 * } 54 * ``` 55 */ 56export class WorkspaceAnalyzer { 57 private rootPath: string; 58 private workspaceConfig: WorkspaceConfig | null = null; 59 60 constructor(rootPath: string = ".") { 61 this.rootPath = resolve(rootPath); 62 } 63 64 async analyze(): Promise<WorkspaceReport | null> { 65 // Load workspace configuration 66 this.workspaceConfig = await this.loadWorkspaceConfig(); 67 68 if (!this.workspaceConfig?.workspace) { 69 return null; 70 } 71 72 console.log(cyan(bold("\n🏢 Doggo is analyzing the pack!\n"))); 73 console.log(gray(`Root: ${this.rootPath}`)); 74 console.log( 75 gray(`Pack members: ${this.workspaceConfig.workspace.length}\n`), 76 ); 77 78 const members: WorkspaceMemberStats[] = []; 79 80 // Analyze each workspace member 81 for (const memberPath of this.workspaceConfig.workspace) { 82 const fullPath = join(this.rootPath, memberPath); 83 console.log(cyan(`\n🐕 Sniffing ${bold(memberPath)}...`)); 84 85 try { 86 const stats = await this.analyzeMember(fullPath, memberPath); 87 members.push(stats); 88 89 // Output brief summary for this member 90 this.outputMemberSummary(stats); 91 } catch (error) { 92 console.error(red(` ✗ Doggo couldn't sniff ${memberPath}: ${error}`)); 93 // Add failed member with zero stats 94 members.push({ 95 name: memberPath, 96 path: fullPath, 97 hasDenoJson: false, 98 hasExports: false, 99 stats: { 100 total: 0, 101 documented: 0, 102 undocumented: 0, 103 percentage: 0, 104 }, 105 }); 106 } 107 } 108 109 // Calculate aggregate statistics 110 const aggregate = this.calculateAggregate(members); 111 const report: WorkspaceReport = { members, aggregate }; 112 113 // Output workspace summary 114 this.outputWorkspaceSummary(report); 115 116 return report; 117 } 118 119 private async loadWorkspaceConfig(): Promise<WorkspaceConfig | null> { 120 // Try deno.json first 121 const denoJsonPath = join(this.rootPath, "deno.json"); 122 try { 123 const content = await Deno.readTextFile(denoJsonPath); 124 const config = JSON.parse(content) as WorkspaceConfig; 125 if (config.workspace) { 126 console.log(gray(`Found workspace configuration in deno.json`)); 127 return config; 128 } 129 } catch { 130 // Not found or parse error 131 } 132 133 // Try deno.jsonc 134 const denoJsoncPath = join(this.rootPath, "deno.jsonc"); 135 try { 136 const content = await Deno.readTextFile(denoJsoncPath); 137 // Simple JSONC parsing - remove comments 138 const jsonContent = content 139 .split("\n") 140 .map((line) => { 141 const commentIndex = line.indexOf("//"); 142 return commentIndex > -1 ? line.slice(0, commentIndex) : line; 143 }) 144 .join("\n") 145 .replace(/\/\*[\s\S]*?\*\//g, ""); 146 const config = JSON.parse(jsonContent) as WorkspaceConfig; 147 if (config.workspace) { 148 console.log(gray(`Found workspace configuration in deno.jsonc`)); 149 return config; 150 } 151 } catch { 152 // Not found or parse error 153 } 154 155 return null; 156 } 157 158 private async analyzeMember( 159 memberPath: string, 160 memberName: string, 161 ): Promise<WorkspaceMemberStats> { 162 // Use the core module to analyze the member 163 const result = await analyzeDirectory(memberPath); 164 165 return { 166 name: memberName, 167 path: memberPath, 168 hasDenoJson: result.hasDenoJson, 169 hasExports: result.hasExports, 170 exportPath: result.exportPath, 171 stats: { 172 total: result.stats.total, 173 documented: result.stats.documented, 174 undocumented: result.stats.undocumented, 175 percentage: result.stats.percentage, 176 }, 177 byType: result.stats.byType, 178 }; 179 } 180 181 private outputMemberSummary(stats: WorkspaceMemberStats): void { 182 const { name, hasExports, exportPath, stats: s } = stats; 183 184 const percentageColor = s.percentage >= 80 185 ? green 186 : s.percentage >= 60 187 ? yellow 188 : red; 189 190 const indicator = s.percentage >= 80 191 ? "✓" 192 : s.percentage >= 60 193 ? "⚡" 194 : "✗"; 195 const indicatorColor = s.percentage >= 80 196 ? green 197 : s.percentage >= 60 198 ? yellow 199 : red; 200 201 console.log( 202 ` ${indicatorColor(indicator)} ${name.padEnd(20)} ${ 203 s.total.toString().padStart(3) 204 } exports, ${percentageColor(bold(`${s.percentage}%`))} documented`, 205 ); 206 207 if (hasExports && exportPath) { 208 console.log(gray(` └─ Entry: ${exportPath}`)); 209 } 210 } 211 212 private calculateAggregate( 213 members: WorkspaceMemberStats[], 214 ): WorkspaceReport["aggregate"] { 215 const totalMembers = members.length; 216 const totalExports = members.reduce((sum, m) => sum + m.stats.total, 0); 217 const totalDocumented = members.reduce( 218 (sum, m) => sum + m.stats.documented, 219 0, 220 ); 221 const totalUndocumented = members.reduce( 222 (sum, m) => sum + m.stats.undocumented, 223 0, 224 ); 225 226 // Calculate weighted average percentage 227 const averagePercentage = totalExports > 0 228 ? Math.round((totalDocumented / totalExports) * 100) 229 : 0; 230 231 return { 232 totalMembers, 233 totalExports, 234 totalDocumented, 235 totalUndocumented, 236 averagePercentage, 237 }; 238 } 239 240 private outputWorkspaceSummary(report: WorkspaceReport): void { 241 const { members, aggregate } = report; 242 243 console.log("\n" + cyan(bold("📊 Pack Summary"))); 244 console.log(gray("─".repeat(60))); 245 246 // Overall stats 247 console.log(`\n ${bold("Pack Statistics:")}`); 248 console.log( 249 ` Pack Members: ${bold(aggregate.totalMembers.toString())}`, 250 ); 251 console.log( 252 ` Total Exports: ${bold(aggregate.totalExports.toString())}`, 253 ); 254 console.log( 255 ` Documented: ${green(aggregate.totalDocumented.toString())}`, 256 ); 257 console.log( 258 ` Undocumented: ${red(aggregate.totalUndocumented.toString())}`, 259 ); 260 261 const percentageColor = aggregate.averagePercentage >= 80 262 ? green 263 : aggregate.averagePercentage >= 60 264 ? yellow 265 : red; 266 console.log( 267 ` Coverage: ${ 268 percentageColor(bold(`${aggregate.averagePercentage}%`)) 269 }`, 270 ); 271 272 // Member breakdown table 273 console.log(`\n ${bold("Member Breakdown:")}\n`); 274 console.log(gray(" " + "─".repeat(56))); 275 console.log( 276 gray(" │") + " Member".padEnd(20) + gray("│") + " Exports " + 277 gray("│") + " Documented " + gray("│") + " Coverage " + gray("│"), 278 ); 279 console.log(gray(" " + "─".repeat(56))); 280 281 // Sort members by coverage percentage 282 const sortedMembers = [...members].sort((a, b) => 283 b.stats.percentage - a.stats.percentage 284 ); 285 286 for (const member of sortedMembers) { 287 const percentageColor = member.stats.percentage >= 80 288 ? green 289 : member.stats.percentage >= 60 290 ? yellow 291 : red; 292 293 const name = member.name.length > 18 294 ? member.name.substring(0, 15) + "..." 295 : member.name; 296 297 console.log( 298 gray(" │") + 299 ` ${name.padEnd(18)} ` + 300 gray("│") + 301 ` ${member.stats.total.toString().padStart(7)} ` + 302 gray("│") + 303 ` ${member.stats.documented.toString().padStart(10)} ` + 304 gray("│") + 305 ` ${ 306 percentageColor( 307 member.stats.percentage.toString().padStart(7) + "%", 308 ) 309 } ` + 310 gray("│"), 311 ); 312 } 313 console.log(gray(" " + "─".repeat(56))); 314 315 // Identify members needing attention 316 const needsWork = members.filter((m) => m.stats.percentage < 60); 317 const goodCoverage = members.filter((m) => m.stats.percentage >= 80); 318 319 if (needsWork.length > 0) { 320 console.log(`\n ${red("⚠️ Pack members needing training:")}`); 321 for (const member of needsWork) { 322 console.log(` - ${member.name} (${member.stats.percentage}%)`); 323 } 324 } 325 326 if (goodCoverage.length > 0) { 327 console.log(`\n ${green("✨ Kibble-worthy pups:")}`); 328 for (const member of goodCoverage) { 329 if (member.stats.percentage === 100) { 330 console.log(` - ${member.name} ${green("(best in show!)")}`); 331 } else { 332 console.log(` - ${member.name} (${member.stats.percentage}%)`); 333 } 334 } 335 } 336 337 // Final indicator 338 const indicator = this.getWorkspaceIndicator(aggregate.averagePercentage); 339 console.log(`\n ${indicator}\n`); 340 } 341 342 private getWorkspaceIndicator(percentage: number): string { 343 if (percentage === 100) { 344 return green("🏆 Perfect! The whole pack is doing zoomies!"); 345 } else if (percentage >= 90) { 346 return green("✨ Excellent! The pack's tails are wagging!"); 347 } else if (percentage >= 80) { 348 return green("👍 Good pack! Happy woofs all around"); 349 } else if (percentage >= 60) { 350 return yellow("📈 The pack needs more training (and treats)"); 351 } else if (percentage >= 40) { 352 return red("⚠️ Poor pack coverage (Collective sad puppy eyes)"); 353 } else { 354 return red("🚨 Critical! The whole pack is hiding"); 355 } 356 } 357} 358 359// CLI entry point 360async function main() { 361 const analyzer = new WorkspaceAnalyzer(); 362 const report = await analyzer.analyze(); 363 364 if (!report) { 365 console.log(yellow("\n⚠️ Woof! No pack configuration found.")); 366 console.log( 367 gray( 368 "Doggo was looking for 'workspace' field in deno.json or deno.jsonc\n", 369 ), 370 ); 371 Deno.exit(1); 372 } 373} 374 375if (import.meta.main) { 376 await main(); 377}