80 lines
2.6 KiB
JavaScript
80 lines
2.6 KiB
JavaScript
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('<meta name="viewport"')) errors.push(`${file}: missing viewport metadata`)
|
|
if (html.includes('<heading')) errors.push(`${file}: contains a nonstandard <heading> 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.`)
|
|
}
|