respira/vite.config.mts
Jan-Henrik Bruhn 0dfc8b731b feature: Implement Web Worker-based pattern conversion with progress tracking
Move Pyodide/PyStitch pattern conversion to a dedicated Web Worker for
non-blocking UI performance. Pyodide now loads in the background on app
startup with real-time progress indicator showing download and initialization
status.

Key changes:
- Create patternConverter.worker.ts with full PES to PEN conversion logic
- Add patternConverterClient.ts singleton for worker management
- Implement progress tracking (0-100%) with step descriptions
- Add inline progress bar in FileUpload component with contextual messaging
- Configure COEP headers for Electron to support Web Workers
- Pass dynamic asset URLs to worker for file:// protocol support
- Update UIStore with progress state management
- Simplify pystitchConverter.ts to delegate to worker client

Benefits:
- Truly non-blocking UI (heavy Python/WASM runs off main thread)
- Real progress feedback during 15MB Pyodide download
- Works in both browser and Electron (dev and production)
- Faster app startup perception with background loading
- Better UX with "waiting" state if user clicks before ready

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 13:34:13 +01:00

158 lines
4.1 KiB
TypeScript

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { viteStaticCopy } from 'vite-plugin-static-copy'
import tailwindcss from '@tailwindcss/vite'
import { dirname, join } from 'path'
import { fileURLToPath } from 'url'
import type { Plugin } from 'vite'
const PYODIDE_EXCLUDE = [
'!**/*.{md,html}',
'!**/*.d.ts',
'!**/node_modules',
]
export function viteStaticCopyPyodide() {
const pyodideDir = dirname(fileURLToPath(import.meta.resolve('pyodide')))
return viteStaticCopy({
targets: [
{
src: [join(pyodideDir, '*').replace(/\\/g, '/')].concat(PYODIDE_EXCLUDE),
dest: 'assets',
},
],
})
}
interface PyPIPackage {
package: string
version: string
}
interface WheelData {
filename: string
buffer: Buffer
}
async function getPyPIWheelUrl(packageName: string, version: string): Promise<{ url: string; filename: string }> {
const pypiUrl = `https://pypi.org/pypi/${packageName}/${version}/json`
const response = await fetch(pypiUrl)
if (!response.ok) {
throw new Error(`Failed to fetch PyPI metadata: ${response.statusText}`)
}
const data = await response.json() as {
urls: Array<{
packagetype: string
filename: string
url: string
}>
}
// Find the wheel file (.whl) for py3-none-any
const wheelFile = data.urls.find((file) =>
file.packagetype === 'bdist_wheel' &&
file.filename.endsWith('-py3-none-any.whl')
)
if (!wheelFile) {
throw new Error(`No py3-none-any wheel found for ${packageName} ${version}`)
}
return {
url: wheelFile.url,
filename: wheelFile.filename,
}
}
export function downloadPyPIWheels(packages: PyPIPackage[]): Plugin {
const wheels: WheelData[] = []
return {
name: 'download-pypi-wheels',
async buildStart() {
try {
// Download all wheels in parallel
const downloadPromises = packages.map(async ({ package: packageName, version }) => {
console.log(`[download-pypi-wheels] Downloading ${packageName}@${version} from PyPI...`)
// Get wheel info from PyPI
const { url, filename } = await getPyPIWheelUrl(packageName, version)
// Download the wheel file
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Failed to download ${filename}: ${response.statusText}`)
}
const arrayBuffer = await response.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
console.log(`[download-pypi-wheels] Successfully downloaded ${filename}`)
return { filename, buffer }
})
const downloadedWheels = await Promise.all(downloadPromises)
wheels.push(...downloadedWheels)
} catch (error) {
console.error(`[download-pypi-wheels] Error downloading wheels:`, error)
throw error
}
},
configureServer(server) {
// Serve wheel files during dev mode
server.middlewares.use((req, res, next) => {
const wheel = wheels.find((w) => req.url === `/${w.filename}`)
if (wheel) {
res.setHeader('Content-Type', 'application/octet-stream')
res.setHeader('Content-Length', wheel.buffer.length)
res.end(wheel.buffer)
return
}
next()
})
},
async generateBundle() {
if (wheels.length === 0) {
throw new Error('No wheel files were downloaded')
}
// Add all wheel files as assets to the bundle
for (const wheel of wheels) {
this.emitFile({
type: 'asset',
fileName: wheel.filename,
source: wheel.buffer,
})
}
},
}
}
// https://vite.dev/config/
export default defineConfig({
plugins: [
react(),
tailwindcss(),
downloadPyPIWheels([
{ package: 'pystitch', version: '1.0.0' },
]),
viteStaticCopyPyodide(),
],
optimizeDeps: {
exclude: ['pyodide'],
},
worker: {
format: 'es',
},
server: {
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
// Mark all dev server resources as same-origin
'Cross-Origin-Resource-Policy': 'same-origin',
},
},
})