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});