Files
bambuddy/frontend/vite.config.ts
T
maziggy d6a3139397 fix(frontend): emit relative asset paths so SPA loads under any subpath (#1195)
Vite's default base of '/' baked absolute asset URLs into the built
  index.html (/assets/..., /manifest.json, /img/..., /sw-register.js),
  so any path-prefixed reverse proxy (Traefik, nginx subpath, Cloudflare
  Tunnel with path routing) served the SPA as a blank white page — the
  browser requested assets from the host root and got HTML or text/plain
  back, triggering MIME-mismatch errors on every stylesheet/script.

  Set base: '' in vite.config.ts so the HTML transform emits relative
  URLs everywhere. Update public/sw-register.js to register('sw.js')
  (relative) so SW scope auto-pins to whatever subpath the document
  loaded from.

  Out of scope: API_BASE in client.ts is still absolute. The supported
  HA embedding path remains Webpage panel + TRUSTED_FRAME_ORIGINS, not
  HA Ingress (subpath-aware SPA bootstrapping has too many failure modes
  around PWA scope, push subscriptions, and deep-link reloads to take on
  in core). Documented explicitly in docker.md.

  Reported by @Spegeli, follow-up to #1167.
2026-05-03 07:33:40 +02:00

107 lines
3.0 KiB
TypeScript

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
import fs from 'fs'
import type { Connect } from 'vite'
// Backend port for dev server proxy (default: 8000)
const backendPort = process.env.BACKEND_PORT || '8000'
const backendUrl = `http://localhost:${backendPort}`
// Absolute path to the gcode_viewer directory at the repo root
const gcodeViewerDir = path.resolve(__dirname, '../gcode_viewer')
// MIME types for static files served from gcode_viewer/
const MIME: Record<string, string> = {
'.html': 'text/html; charset=utf-8',
'.js': 'application/javascript',
'.css': 'text/css',
'.obj': 'model/obj',
'.mtl': 'model/mtl',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.svg': 'image/svg+xml',
'.json': 'application/json',
'.woff': 'font/woff',
'.woff2':'font/woff2',
}
/**
* Vite dev-server plugin: serves ../gcode_viewer/ at /gcode-viewer/
* without needing a proxy to uvicorn. In production uvicorn handles it
* via the StaticFiles mount in main.py.
*/
function serveGcodeViewer() {
return {
name: 'serve-gcode-viewer',
configureServer(server: { middlewares: Connect.Server }) {
server.middlewares.use((req, res, next) => {
const url = req.url ?? ''
if (!url.startsWith('/gcode-viewer')) return next()
// Strip prefix, default to index.html
let rel = url.slice('/gcode-viewer'.length)
if (rel === '' || rel === '/') rel = '/index.html'
// Strip query string
rel = rel.split('?')[0]
const absPath = path.join(gcodeViewerDir, rel)
try {
const stat = fs.statSync(absPath)
if (stat.isFile()) {
const ext = path.extname(absPath).toLowerCase()
res.setHeader('Content-Type', MIME[ext] ?? 'application/octet-stream')
res.end(fs.readFileSync(absPath))
return
}
} catch {
// file not found — fall through to index.html
}
// SPA fallback: serve index.html for any unmatched /gcode-viewer/* path
const index = path.join(gcodeViewerDir, 'index.html')
if (fs.existsSync(index)) {
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.end(fs.readFileSync(index))
return
}
next()
})
},
}
}
export default defineConfig({
// Empty base emits relative asset URLs (./assets/... instead of /assets/...)
// so the built SPA loads correctly when served at any subpath — HA Ingress,
// nginx/Traefik path prefix, Cloudflare Tunnel path routing, etc. (#1195).
base: '',
plugins: [react(), serveGcodeViewer()],
build: {
outDir: '../static',
emptyOutDir: true,
chunkSizeWarningLimit: 3000,
},
server: {
host: '0.0.0.0',
proxy: {
'/api/v1/ws': {
target: backendUrl,
ws: true,
changeOrigin: true,
},
'/api': {
target: backendUrl,
changeOrigin: true,
},
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})