1import tailwindcss from '@tailwindcss/vite';
  2import { sveltekit } from '@sveltejs/kit/vite';
  3import * as fflate from 'fflate';
  4import { readFileSync, writeFileSync, existsSync } from 'fs';
  5import { resolve } from 'path';
  6import { defineConfig } from 'vite';
  7import devtoolsJson from 'vite-plugin-devtools-json';
  8import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
  9
 10const GUIDE_FOR_FRONTEND = `
 11<!--
 12  This is a single file build of the frontend.
 13  It is automatically generated by the build process.
 14  Do not edit this file directly.
 15  To make changes, refer to the "Web UI" section in the README.
 16-->
 17`.trim();
 18
 19const MAX_BUNDLE_SIZE = 2 * 1024 * 1024;
 20
 21/**
 22 * the maximum size of an embedded asset in bytes,
 23 * e.g. maximum size of embedded font (see node_modules/katex/dist/fonts/*.woff2)
 24 */
 25const MAX_ASSET_SIZE = 32000;
 26
 27/** public/index.html.gz minified flag */
 28const ENABLE_JS_MINIFICATION = true;
 29
 30function llamaCppBuildPlugin() {
 31	return {
 32		name: 'llamacpp:build',
 33		apply: 'build' as const,
 34		closeBundle() {
 35			// Ensure the SvelteKit adapter has finished writing to ../public
 36			setTimeout(() => {
 37				try {
 38					const indexPath = resolve('../public/index.html');
 39					const gzipPath = resolve('../public/index.html.gz');
 40
 41					if (!existsSync(indexPath)) {
 42						return;
 43					}
 44
 45					let content = readFileSync(indexPath, 'utf-8');
 46
 47					const faviconPath = resolve('static/favicon.svg');
 48					if (existsSync(faviconPath)) {
 49						const faviconContent = readFileSync(faviconPath, 'utf-8');
 50						const faviconBase64 = Buffer.from(faviconContent).toString('base64');
 51						const faviconDataUrl = `data:image/svg+xml;base64,${faviconBase64}`;
 52
 53						content = content.replace(/href="[^"]*favicon\.svg"/g, `href="${faviconDataUrl}"`);
 54
 55						console.log('✓ Inlined favicon.svg as base64 data URL');
 56					}
 57
 58					content = content.replace(/\r/g, '');
 59					content = GUIDE_FOR_FRONTEND + '\n' + content;
 60
 61					const compressed = fflate.gzipSync(Buffer.from(content, 'utf-8'), { level: 9 });
 62
 63					compressed[0x4] = 0;
 64					compressed[0x5] = 0;
 65					compressed[0x6] = 0;
 66					compressed[0x7] = 0;
 67					compressed[0x9] = 0;
 68
 69					if (compressed.byteLength > MAX_BUNDLE_SIZE) {
 70						throw new Error(
 71							`Bundle size is too large (${Math.ceil(compressed.byteLength / 1024)} KB).\n` +
 72								`Please reduce the size of the frontend or increase MAX_BUNDLE_SIZE in vite.config.ts.\n`
 73						);
 74					}
 75
 76					writeFileSync(gzipPath, compressed);
 77					console.log('✓ Created index.html.gz');
 78				} catch (error) {
 79					console.error('Failed to create gzip file:', error);
 80				}
 81			}, 100);
 82		}
 83	};
 84}
 85
 86export default defineConfig({
 87	resolve: {
 88		alias: {
 89			'katex-fonts': resolve('node_modules/katex/dist/fonts')
 90		}
 91	},
 92	build: {
 93		assetsInlineLimit: MAX_ASSET_SIZE,
 94		chunkSizeWarningLimit: 3072,
 95		minify: ENABLE_JS_MINIFICATION
 96	},
 97	css: {
 98		preprocessorOptions: {
 99			scss: {
100				additionalData: `
101					$use-woff2: true;
102					$use-woff: false;
103					$use-ttf: false;
104				`
105			}
106		}
107	},
108	plugins: [tailwindcss(), sveltekit(), devtoolsJson(), llamaCppBuildPlugin()],
109	test: {
110		projects: [
111			{
112				extends: './vite.config.ts',
113				test: {
114					name: 'client',
115					environment: 'browser',
116					browser: {
117						enabled: true,
118						provider: 'playwright',
119						instances: [{ browser: 'chromium' }]
120					},
121					include: ['tests/client/**/*.svelte.{test,spec}.{js,ts}'],
122					setupFiles: ['./vitest-setup-client.ts']
123				}
124			},
125			{
126				extends: './vite.config.ts',
127				test: {
128					name: 'unit',
129					environment: 'node',
130					include: ['tests/unit/**/*.{test,spec}.{js,ts}']
131				}
132			},
133			{
134				extends: './vite.config.ts',
135				test: {
136					name: 'ui',
137					environment: 'browser',
138					browser: {
139						enabled: true,
140						provider: 'playwright',
141						instances: [{ browser: 'chromium', headless: true }]
142					},
143					include: ['tests/stories/**/*.stories.{js,ts,svelte}'],
144					setupFiles: ['./.storybook/vitest.setup.ts']
145				},
146				plugins: [
147					storybookTest({
148						storybookScript: 'pnpm run storybook --no-open'
149					})
150				]
151			}
152		]
153	},
154
155	server: {
156		proxy: {
157			'/v1': 'http://localhost:8080',
158			'/props': 'http://localhost:8080',
159			'/models': 'http://localhost:8080'
160		},
161		headers: {
162			'Cross-Origin-Embedder-Policy': 'require-corp',
163			'Cross-Origin-Opener-Policy': 'same-origin'
164		}
165	}
166});