import { readdir, readFile, stat } from 'node:fs/promises'; import { join } from 'node:path'; const generatedRoot = '.generated'; const walk = async (directory) => { const files = []; for (const entry of await readdir(directory, { withFileTypes: true })) { const path = join(directory, entry.name); if (entry.isDirectory()) files.push(...await walk(path)); else files.push(path); } return files; }; const exists = async (path) => { try { await stat(path); return true; } catch { return false; } }; const errors = []; const htmlFiles = (await walk(generatedRoot)).filter((file) => file.endsWith('.html')); const clientSource = await readFile('src/main.js', 'utf8'); const builtInPrismLanguages = new Set(['markup', 'html', 'xml', 'svg', 'css', 'clike', 'javascript', 'js', 'plain', 'plaintext', 'text', 'txt']); const prismLanguageAliases = { sh: 'bash', shell: 'bash' }; for (const file of htmlFiles) { const html = await readFile(file, 'utf8'); const ids = [...html.matchAll(/\sid="([^"]+)"/g)].map((match) => match[1]); const duplicateIds = ids.filter((id, index) => ids.indexOf(id) !== index); if (duplicateIds.length > 0) errors.push(`${file}: duplicate IDs: ${[...new Set(duplicateIds)].join(', ')}`); if (!html.includes(' element`); for (const [, fragment] of html.matchAll(/href="#([^"]+)"/g)) { if (!ids.includes(fragment)) errors.push(`${file}: missing fragment target #${fragment}`); } for (const [, href] of html.matchAll(/href="(\/[^"]*)"/g)) { if (href === '/') continue; const pathname = href.split(/[?#]/, 1)[0]; let target; if (pathname.startsWith('/src/')) target = `.${pathname}`; else if (pathname.startsWith('/assets/') || pathname === '/favicon.ico') target = `public${pathname}`; else target = pathname.endsWith('/') ? join(generatedRoot, pathname, 'index.html') : join(generatedRoot, pathname); if (!await exists(target)) errors.push(`${file}: broken internal link ${href}`); } for (const [, language] of html.matchAll(/class="language-([\w-]+)"/g)) { if (builtInPrismLanguages.has(language)) continue; const component = prismLanguageAliases[language] ?? language; if (!clientSource.includes(`prism-${component}`)) errors.push(`${file}: Prism grammar is not imported for ${language}`); } } if (errors.length > 0) { console.error(errors.join('\n')); process.exitCode = 1; } else { console.log(`Checked ${htmlFiles.length} generated pages.`); }