import { readdir, readFile, stat } from 'node:fs/promises'; import path from 'node:path'; const dist = path.resolve('dist'); const allowedPreservedRoutes = new Set(['/fleeting/', '/reconcile/']); const failures = []; async function walk(dir) { const entries = await readdir(dir, { withFileTypes: true }); const files = []; for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { files.push(...(await walk(fullPath))); } else { files.push(fullPath); } } return files; } async function exists(file) { try { return (await stat(file)).isFile(); } catch { return false; } } async function targetExists(pathname) { if (allowedPreservedRoutes.has(pathname)) return true; const safePath = path .normalize(decodeURIComponent(pathname)) .replace(/^\/+/, '') .replace(/^(\.\.(\/|\\|$))+/, ''); const candidate = path.join(dist, safePath); const candidates = [ candidate, path.join(candidate, 'index.html'), path.join(dist, `${safePath}.html`), ]; for (const file of candidates) { if (await exists(file)) return true; } return false; } try { await stat(dist); } catch { throw new Error('dist/ does not exist. Run npm run build first.'); } const files = await walk(dist); const checkedFiles = files.filter((file) => /\.(html|xml|css|webmanifest)$/.test(file)); function pagePathname(file) { const rel = path.relative(dist, file).replaceAll(path.sep, '/'); if (rel === 'index.html') return '/'; if (rel.endsWith('/index.html')) return `/${rel.slice(0, -'index.html'.length)}`; return `/${rel}`; } function collectUrlReferences(body, rel) { const urls = []; for (const match of body.matchAll(/\b(?:href|src|poster)=["']([^"']+)["']/g)) { urls.push(match[1]); } for (const match of body.matchAll(/\bsrcset=["']([^"']+)["']/g)) { for (const candidate of match[1].split(',')) { const url = candidate.trim().split(/\s+/)[0]; if (url) urls.push(url); } } if (rel.endsWith('.css')) { for (const match of body.matchAll(/url\(\s*['"]?([^'")]+)['"]?\s*\)/g)) { urls.push(match[1]); } } if (rel.endsWith('.webmanifest')) { try { const manifest = JSON.parse(body); for (const key of ['start_url', 'scope']) { if (typeof manifest[key] === 'string') urls.push(manifest[key]); } for (const icon of manifest.icons ?? []) { if (typeof icon?.src === 'string') urls.push(icon.src); } for (const screenshot of manifest.screenshots ?? []) { if (typeof screenshot?.src === 'string') urls.push(screenshot.src); } } catch { failures.push(`${rel}: invalid web manifest JSON`); } } return urls; } for (const file of checkedFiles) { const body = await readFile(file, 'utf8'); const rel = path.relative(dist, file); const baseUrl = new URL(pagePathname(file), 'https://schmelczer.dev'); for (const raw of collectUrlReferences(body, rel)) { if (/^(mailto:|tel:|data:)/i.test(raw)) continue; let parsed; try { parsed = new URL(raw, baseUrl); } catch { failures.push(`${rel}: invalid URL ${raw}`); continue; } if (parsed.origin !== 'https://schmelczer.dev') continue; if (!(await targetExists(parsed.pathname))) { failures.push(`${rel}: missing local target ${parsed.pathname}`); } } } if (failures.length > 0) { console.error(failures.join('\n')); process.exit(1); } console.log('No missing local href/src targets found in dist/.');