canvas-website/vite.config.ts

233 lines
7.4 KiB
TypeScript

import { defineConfig, loadEnv } from "vite"
import react from "@vitejs/plugin-react"
import wasm from "vite-plugin-wasm"
import topLevelAwait from "vite-plugin-top-level-await"
import { VitePWA } from "vite-plugin-pwa"
export default defineConfig(({ mode }) => {
// Load env file based on `mode` in the current working directory.
// Set the third parameter to '' to load all env regardless of the `VITE_` prefix.
const env = loadEnv(mode, process.cwd(), '')
// Set the worker URL to localhost for local development
process.env.VITE_TLDRAW_WORKER_URL = 'http://localhost:5172'
return {
envPrefix: ["VITE_"],
plugins: [
react(),
wasm(),
topLevelAwait(),
VitePWA({
registerType: 'autoUpdate',
injectRegister: 'auto',
workbox: {
// Force the service worker to take control immediately
skipWaiting: true,
clientsClaim: true,
// Cache all static assets
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2,wasm}'],
// Increase the limit for large chunks (Board is ~8MB with tldraw, automerge, etc.)
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024, // 10MB
// Runtime caching for dynamic requests
runtimeCaching: [
{
// Cache API responses with network-first strategy
urlPattern: /^https?:\/\/.*\/api\/.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: {
maxEntries: 100,
maxAgeSeconds: 60 * 60 * 24, // 24 hours
},
networkTimeoutSeconds: 10,
},
},
{
// Cache fonts
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'google-fonts-cache',
expiration: {
maxEntries: 10,
maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year
},
},
},
{
urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'gstatic-fonts-cache',
expiration: {
maxEntries: 10,
maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year
},
},
},
],
},
manifest: {
name: 'Jeff Emmett Canvas',
short_name: 'Canvas',
description: 'Collaborative canvas for research and creativity',
theme_color: '#1a1a2e',
background_color: '#1a1a2e',
display: 'standalone',
start_url: '/',
icons: [
{
src: '/pwa-192x192.svg',
sizes: '192x192',
type: 'image/svg+xml',
},
{
src: '/pwa-512x512.svg',
sizes: '512x512',
type: 'image/svg+xml',
},
{
src: '/pwa-512x512.svg',
sizes: '512x512',
type: 'image/svg+xml',
purpose: 'maskable',
},
],
},
devOptions: {
// Enable SW in development for testing
enabled: true,
type: 'module',
},
}),
],
server: {
host: "0.0.0.0",
port: 5173,
strictPort: true,
// Force IPv4 to ensure compatibility with WSL2 and remote devices
listen: "0.0.0.0",
// Configure HMR to use the client's hostname for WebSocket connections
// This allows HMR to work from any network (localhost, LAN, Tailscale)
hmr: {
// Use 'clientPort' to let client determine the correct host
clientPort: 5173,
},
// Proxy API requests to the worker server
proxy: {
'/api': {
target: 'http://localhost:5172',
changeOrigin: true,
},
},
},
build: {
sourcemap: false, // Disable sourcemaps in production to reduce bundle size
rollupOptions: {
output: {
// Manual chunk splitting for large libraries to improve load times
manualChunks(id) {
// Core React libraries - load first
if (id.includes('node_modules/react') ||
id.includes('node_modules/react-dom') ||
id.includes('node_modules/react-router')) {
return 'react-vendor';
}
// tldraw core - split from shapes
if (id.includes('node_modules/tldraw') ||
id.includes('node_modules/@tldraw')) {
return 'tldraw';
}
// Automerge - CRDT sync library
// Note: automerge-repo-react-hooks must NOT be in this chunk as it depends on React
if (id.includes('node_modules/@automerge') &&
!id.includes('automerge-repo-react-hooks')) {
return 'automerge';
}
// AI SDKs (lazy load)
if (id.includes('node_modules/@anthropic-ai') ||
id.includes('node_modules/openai') ||
id.includes('node_modules/ai/')) {
return 'ai-sdks';
}
// ML/transformers (VERY large, lazy loaded)
if (id.includes('node_modules/@xenova')) {
return 'ml-libs';
}
// Markdown editors
if (id.includes('node_modules/@uiw/react-md-editor') ||
id.includes('node_modules/cherry-markdown') ||
id.includes('node_modules/marked') ||
id.includes('node_modules/react-markdown')) {
return 'markdown';
}
// CodeMirror (used by markdown editors)
if (id.includes('node_modules/@codemirror') ||
id.includes('node_modules/codemirror')) {
return 'codemirror';
}
// Daily video chat
if (id.includes('node_modules/@daily-co')) {
return 'daily-video';
}
// html2canvas (screenshots)
if (id.includes('node_modules/html2canvas')) {
return 'html2canvas';
}
// ONNX runtime (ML inference)
if (id.includes('node_modules/onnxruntime')) {
return 'onnx';
}
// DOMPurify and sanitizers
if (id.includes('node_modules/dompurify') ||
id.includes('node_modules/isomorphic-dompurify')) {
return 'sanitizers';
}
},
},
},
chunkSizeWarningLimit: 1000, // Warn on chunks larger than 1MB
},
base: "/",
publicDir: "src/public",
resolve: {
alias: {
"@": "/src",
},
},
define: {
// Worker URL is now handled dynamically in Board.tsx based on window.location.hostname
// This ensures remote devices connect to the correct worker IP
__DAILY_API_KEY__: JSON.stringify(process.env.VITE_DAILY_API_KEY || env.VITE_DAILY_API_KEY)
},
optimizeDeps: {
include: [
'@xenova/transformers',
'@xterm/xterm',
'@xterm/addon-fit'
],
exclude: [
// Exclude problematic modules from pre-bundling
]
},
assetsInclude: [
// Include WebAssembly files
'**/*.wasm',
'**/*.onnx',
'**/*.bin'
]
}
})