#!/usr/bin/env node /** * Post-build: inline critical CSS in dist/*.html (SvelteKit adapter-static output). * Runs after vite build; Critters reads/writes relative to dist/. * * Critters with preload:'swap' adds onload but does not set rel="preload" as="style", * so the link stays render-blocking. We fix that in postProcessSwapLinks(). */ import Critters from 'critters'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.join(__dirname, '..'); const DIST = path.join(ROOT, 'dist'); /** * Critters leaves rel="stylesheet" on swap links; change to rel="preload" as="style" * so the full CSS loads async and only applies on load (non-blocking). */ function postProcessSwapLinks(html) { return html.replace(/]*)>/gi, (full, attrs) => { if ( !/rel="stylesheet"/i.test(attrs) || !/onload="this\.rel='stylesheet'"/i.test(attrs) ) { return full; } const fixed = attrs .replace(/\brel="stylesheet"\s*/i, 'rel="preload" as="style" ') .replace( /\bonload="this\.rel='stylesheet'"/i, 'onload="this.onload=null;this.rel=\'stylesheet\'"', ); return ``; }); } async function main() { if (!fs.existsSync(DIST)) { console.error('dist/ not found. Run vite build first.'); process.exit(1); } const critters = new Critters({ path: DIST, preload: 'default', noscriptFallback: true, pruneSource: false, logLevel: 'warn', }); const files = fs.readdirSync(DIST).filter((f) => f.endsWith('.html')); for (const file of files) { const filePath = path.join(DIST, file); let html = fs.readFileSync(filePath, 'utf8'); html = await critters.process(html); // html = postProcessSwapLinks(html); fs.writeFileSync(filePath, html, 'utf8'); console.log('✓ Critical CSS inlined → dist/' + file); } console.log('Critical CSS step complete.'); } main().catch((err) => { console.error(err); process.exit(1); });