import * as fs from 'fs'; import * as path from 'path'; import * as cheerio from 'cheerio'; import * as ts from 'typescript'; import { glob } from 'glob'; // Configuration const SRC_DIR = 'src'; async function validateHtmlFiles() { console.log(`[HTML Validator] Scanning ${SRC_DIR} for HTML files...`); // Find all HTML files const htmlFiles = glob.sync(`${SRC_DIR}/**/*.html`); let hasErrors = false; for (const file of htmlFiles) { const absolutPath = path.resolve(file); const content = fs.readFileSync(absolutPath, 'utf-8'); // Load with source location info enabled // Cast options to any to avoid TS version mismatches with cheerio types const options: any = { sourceCodeLocationInfo: true }; const $ = cheerio.load(content, options); const scripts = $('script').toArray(); for (const element of scripts) { // Cast to any to access startIndex safely const node = element as any; // Skip external scripts if ($(element).attr('src')) continue; const scriptContent = $(element).html(); if (!scriptContent) continue; // Determine start line of the script tag in the original file // Cheerio (htmlparser2) location info: const loc = node.startIndex !== undefined ? getLineNumber(content, node.startIndex) : 1; // Validate Syntax using TypeScript Compiler API const sourceFile = ts.createSourceFile( 'virtual.js', scriptContent, ts.ScriptTarget.ES2020, true, // setParentNodes ts.ScriptKind.JS ); // Cast to any because parseDiagnostics might not be in the public interface depending on version const sf: any = sourceFile; if (sf.parseDiagnostics && sf.parseDiagnostics.length > 0) { hasErrors = true; console.error(`\n❌ Syntax Error in ${file}`); sf.parseDiagnostics.forEach((diag: any) => { const { line, character } = sourceFile.getLineAndCharacterOfPosition(diag.start!); const message = ts.flattenDiagnosticMessageText(diag.messageText, '\n'); // Adjust line number: Script Start line + Error line inside script // Note: 'line' is 0-indexed relative to script start const visualLine = loc + line; console.error(` Line ${visualLine}: ${message}`); // Show snippet const lines = scriptContent.split('\n'); if (lines[line]) { console.error(` > ${lines[line].trim()}\n`); } }); } } } if (hasErrors) { console.error(`\n[HTML Validator] Failed. Syntax errors detected.`); process.exit(1); } else { console.log(`[HTML Validator] Passed. All HTML scripts are valid.`); } } // Helper to calculate line number from char index function getLineNumber(fullText: string, index: number): number { return fullText.substring(0, index).split('\n').length; } // Check if run directly if (require.main === module) { validateHtmlFiles().catch(err => { console.error("Validator crashed:", err); process.exit(1); }); }