import { readdir, readFile, stat } from 'node:fs/promises'; import path from 'node:path'; const dist = path.resolve('dist'); 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; } try { await stat(dist); } catch { throw new Error('dist/ does not exist. Run npm run build first.'); } const files = await walk(dist); const jsFiles = files.filter((file) => file.endsWith('.js')); if (jsFiles.length > 0) { failures.push( `Unexpected JavaScript assets:\n${jsFiles.map((file) => `- ${file}`).join('\n')}` ); } // Script tags are only allowed if they declare one of these safe `type` // attributes (or are tagged with `data-theme-script`). All other scripts, // including untyped ones, which default to executable JavaScript, are flagged. const SAFE_SCRIPT_TYPES = new Set([ 'application/ld+json', 'importmap', 'speculationrules', ]); function isSafeScriptTag(tag) { if (tag.includes('data-theme-script')) return true; if (tag.includes('data-thumbnail-iframe-script')) return true; const typeMatch = tag.match(/\btype=["']([^"']+)["']/i); if (!typeMatch) return false; return SAFE_SCRIPT_TYPES.has(typeMatch[1].trim().toLowerCase()); } for (const file of files.filter((candidate) => candidate.endsWith('.html'))) { const html = await readFile(file, 'utf8'); const scripts = (html.match(/]*>/gi) ?? []).filter( (tag) => !isSafeScriptTag(tag) ); if (scripts.length) { failures.push(`Unexpected script tag in ${file}:\n${scripts.join('\n')}`); } // Inline event handlers (onclick=, onload=, etc.) execute JavaScript even // without a