import { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import tailwindcss from '@tailwindcss/vite'; import { viteSingleFile } from 'vite-plugin-singlefile'; import { defineConfig } from 'vite'; import { fileURLToPath } from 'node:url'; import { readFileSync } from 'node:fs'; /** Webfonts the shared theme declares, and where their bytes actually live. */ const FONTS = ['Inter', 'Fraunces']; /** * Inline the webfonts the shared theme CSS references by ABSOLUTE path. * * `src/tailwind-theme.css` is the design-token source of truth for both the live app and this * viewer, and it declares `src: url('/fonts/Inter.woff2')`. That is correct for the app, which * serves `static/fonts/` from the site root — but the keepsake is opened from `file://` off a USB * stick or a Downloads folder, where `/fonts/...` resolves to the root of the guest's DISK. Both * requests 404, silently: `font-display: swap` means the viewer renders in a fallback system font * with no error, so nothing on the server side can ever report it. The keepsake is the one artifact * the whole event exists to produce, and it was shipping without the typography it was designed in. * * Fixing it in the shared CSS would break the app (a data URI there would inline ~154 KB into every * page load for no reason), and shipping a `fonts/` folder beside `index.html` would give the guest * a directory they can break by moving one file. So the substitution belongs HERE, in the build that * knows its output has no origin: rewrite the emitted HTML only. * * Runs in `generateBundle` rather than `transformIndexHtml` because `viteSingleFile` inlines the * stylesheet during the latter — the `url()` we need to rewrite does not exist in the HTML until * after it has run. */ function inlineThemeFonts() { return { name: 'eventsnap:inline-theme-fonts', enforce: 'post', generateBundle(_options, bundle) { const html = bundle['index.html']; if (!html || typeof html.source !== 'string') return; for (const family of FONTS) { const bytes = readFileSync( fileURLToPath(new URL(`../static/fonts/${family}.woff2`, import.meta.url)) ); const uri = `data:font/woff2;base64,${bytes.toString('base64')}`; const before = html.source; html.source = html.source.replaceAll(`/fonts/${family}.woff2`, uri); // A silent no-op here is the exact failure this plugin exists to prevent, and it // would come back the moment the theme renames a font or switches to a CDN. Fail // the build instead of shipping another keepsake in Times New Roman. if (html.source === before) { this.error( `inline-theme-fonts: no reference to /fonts/${family}.woff2 in the built ` + `keepsake. The shared theme CSS changed how it loads webfonts — update ` + `FONTS in vite.standalone.config.js to match, or the offline viewer will ` + `render in a fallback font.` ); } } } }; } // Builds the keepsake viewer as ONE self-contained index.html (all JS + CSS // inlined) so it renders when opened via file://. Uses the plain Svelte plugin // (not SvelteKit) because SvelteKit emits multiple module entry points, which // cannot be inlined into a single file. export default defineConfig({ plugins: [ tailwindcss(), svelte({ configFile: false, preprocess: vitePreprocess(), compilerOptions: { runes: true } }), viteSingleFile(), inlineThemeFonts() ], resolve: { alias: { $lib: fileURLToPath(new URL('./src/lib', import.meta.url)) } }, build: { outDir: fileURLToPath(new URL('../../backend/static/export-viewer', import.meta.url)), emptyOutDir: true, target: 'es2020' } });