import { existsSync, readFileSync, readdirSync } from "node:fs"; import { join, resolve, extname } from "node:path"; import { lintHyperframeHtml, type HyperframeLintResult } from "@hyperframes/core/lint"; import type { HyperframeLintFinding } from "@hyperframes/core/lint"; import type { ProjectDir } from "./project.js"; export interface ProjectLintResult { results: Array<{ file: string; result: HyperframeLintResult }>; totalErrors: number; totalWarnings: number; totalInfos: number; } const AUDIO_EXTENSIONS = new Set([".mp3", ".wav", ".aac", ".ogg", ".m4a", ".flac", ".opus"]); /** * Lint the root index.html and all sub-compositions in the compositions/ directory. * Returns aggregated results across all files. */ export function lintProject(project: ProjectDir): ProjectLintResult { const results: Array<{ file: string; result: HyperframeLintResult }> = []; let totalErrors = 0; let totalWarnings = 0; let totalInfos = 0; // Lint root composition const rootHtml = readFileSync(project.indexPath, "utf-8"); const rootResult = lintHyperframeHtml(rootHtml, { filePath: project.indexPath }); results.push({ file: "index.html", result: rootResult }); totalErrors += rootResult.errorCount; totalWarnings += rootResult.warningCount; totalInfos += rootResult.infoCount; // Lint sub-compositions in compositions/ directory, collecting HTML for project-level checks const allHtmlSources = [rootHtml]; const compositionsDir = resolve(project.dir, "compositions"); if (existsSync(compositionsDir)) { const files = readdirSync(compositionsDir).filter((f) => f.endsWith(".html")); for (const file of files) { const filePath = join(compositionsDir, file); const html = readFileSync(filePath, "utf-8"); allHtmlSources.push(html); const result = lintHyperframeHtml(html, { filePath, isSubComposition: true }); results.push({ file: `compositions/${file}`, result }); totalErrors += result.errorCount; totalWarnings += result.warningCount; totalInfos += result.infoCount; } } // ── Project-level checks ────────────────────────────────────────────── const projectFindings = [ ...lintProjectAudioFiles(project.dir, allHtmlSources), ...lintAudioSrcNotFound(project.dir, allHtmlSources), ...lintMultipleRootCompositions(project.dir), ...lintDuplicateAudioTracks(allHtmlSources), ]; if (projectFindings.length > 0) { // Append project-level findings to the root index.html result for (const finding of projectFindings) { rootResult.findings.push(finding); if (finding.severity === "error") { rootResult.errorCount++; rootResult.ok = false; totalErrors++; } else if (finding.severity === "warning") { rootResult.warningCount++; totalWarnings++; } else { rootResult.infoCount++; totalInfos++; } } } return { results, totalErrors, totalWarnings, totalInfos }; } /** * Check for audio files in the project directory that have no corresponding *