/** * Inline critical CSS into built HTML using Beasties. * Run after vite build; reads/writes build/. */ import { readFileSync, writeFileSync, readdirSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import { log, error } from 'node:console'; import process from 'node:process'; import Beasties from 'beasties'; const __dirname = dirname(fileURLToPath(import.meta.url)); const buildDir = join(__dirname, '..', 'build'); function getFiles(dir, ext, files = []) { for (const name of readdirSync(dir, { withFileTypes: true })) { const full = join(dir, name.name); if (name.isDirectory()) getFiles(full, ext, files); else if (name.name.endsWith(ext)) files.push(full); } return files; } const beasties = new Beasties({ path: buildDir, preload: 'default', logLevel: 'warn', inlineThreshold: 50 * 1024, minimumExternalSize: 50 * 1024 }); /** * Remove SvelteKit wrapper div and Svelte SSR fragment comments from HTML. * With csr = false these add noise and the inline style is unnecessary. */ function cleanHtml(html) { // Remove extra SvelteKit body attribute: data-sveltekit-preload-data="hover" html = html.replace(//i, ''); // Remove SvelteKit's root wrapper:
...
html = html.replace(//i, ''); const bodyEnd = html.indexOf(''); if (bodyEnd !== -1) { const beforeBody = html.slice(0, bodyEnd); const lastDiv = beforeBody.lastIndexOf(''); if (lastDiv !== -1) { html = beforeBody.slice(0, lastDiv) + beforeBody.slice(lastDiv + 6) + html.slice(bodyEnd); } } // Remove Svelte fragment/hydration comments (unused when csr = false) html = html .replace(//g, '') .replace(//g, '') .replace(//g, '') .replace(//g, '') .replace(//g, ''); return html; } async function main() { const htmlFiles = getFiles(buildDir, '.html'); for (const htmlFile of htmlFiles) { let html = readFileSync(htmlFile, 'utf8'); html = await beasties.process(html); html = cleanHtml(html); writeFileSync(htmlFile, html); } log('Critical CSS inlined:', htmlFiles.length, 'file(s)'); } main().catch((err) => { error(err); process.exit(1); });