mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 02:13:41 +00:00
Major changes: - Add Electron main process and preload scripts with Web Bluetooth support - Implement platform abstraction layer for storage and file services - Create BluetoothDevicePicker component for device selection UI - Migrate from electron-builder to Electron Forge for packaging - Configure Vite for dual browser/Electron builds - Add native file dialogs and persistent storage via electron-store - Hide menu bar for cleaner desktop app appearance The app now works in both browser (npm run dev) and Electron (npm run start). Package with 'npm run package' or create installers with 'npm run make'. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
153 lines
4 KiB
TypeScript
153 lines
4 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'],
|
|
},
|
|
server: {
|
|
headers: {
|
|
'Cross-Origin-Opener-Policy': 'same-origin',
|
|
'Cross-Origin-Embedder-Policy': 'require-corp',
|
|
},
|
|
},
|
|
})
|