dea08f9c47
Adds the manifest, icons and service worker that make the app installable, and offers it as a toast once Chrome says it qualifies. Declining snoozes the offer for a month; installing ends it. A waiting service worker never activates on its own. Reloading the page under a listener to swap in a new build would cut the song they are in the middle of, so updates land on the next cold start instead. Audio is kept out of the cache entirely: range requests and multi-megabyte bodies do not belong in a shell cache. Artwork is cached, and the SPA navigation fallback denies /api so it cannot swallow the event stream. Installed on Android the app paints edge to edge, so the transport pads itself past the gesture bar. MediaSession gains setPositionState, which is what gives the notification shade a seek bar that moves. Three things kept the bundle from ever being compressed, each hiding the next: the nginx image ships with gzip off, gzip_proxied defaults to off and skips anything carrying a Via header, and gzip_http_version defaults to 1.1 while the host proxy speaks 1.0. With those fixed and the pages split per route, the first load goes from 555KB to 60KB of app code plus a vendor chunk that survives redeploys. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
90 lines
3.0 KiB
JavaScript
90 lines
3.0 KiB
JavaScript
import { defineConfig } from 'vite'
|
|
import react from '@vitejs/plugin-react'
|
|
import { VitePWA } from 'vite-plugin-pwa'
|
|
|
|
export default defineConfig({
|
|
plugins: [
|
|
react(),
|
|
VitePWA({
|
|
registerType: 'autoUpdate',
|
|
includeAssets: ['favicon-32.png', 'apple-touch-icon.png'],
|
|
manifest: {
|
|
name: 'Muzick',
|
|
short_name: 'Muzick',
|
|
description: 'Self-hosted music player and recommendation engine',
|
|
start_url: '/',
|
|
scope: '/',
|
|
display: 'standalone',
|
|
orientation: 'portrait',
|
|
background_color: '#14110D',
|
|
theme_color: '#14110D',
|
|
icons: [
|
|
{ src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
|
|
{ src: '/icon-512.png', sizes: '512x512', type: 'image/png' },
|
|
{
|
|
src: '/icon-maskable-512.png',
|
|
sizes: '512x512',
|
|
type: 'image/png',
|
|
purpose: 'maskable',
|
|
},
|
|
],
|
|
},
|
|
workbox: {
|
|
globPatterns: ['**/*.{js,css,html,woff2}'],
|
|
|
|
// The SPA fallback must never swallow the API: /api/playback/stream is a
|
|
// long-lived SSE channel and the track endpoints stream audio. Both look
|
|
// like navigations to Workbox unless they are denied here.
|
|
navigateFallback: '/index.html',
|
|
navigateFallbackDenylist: [/^\/api\//],
|
|
|
|
// A waiting service worker would otherwise activate and reload the page
|
|
// mid-song. Letting it wait means updates land on the next cold start,
|
|
// which is the right trade for a player that runs in the background.
|
|
skipWaiting: false,
|
|
clientsClaim: false,
|
|
|
|
runtimeCaching: [
|
|
{
|
|
// Artwork only. Audio is deliberately absent: range requests and
|
|
// multi-megabyte bodies do not belong in the shell cache.
|
|
urlPattern: ({ request }) => request.destination === 'image',
|
|
handler: 'CacheFirst',
|
|
options: {
|
|
cacheName: 'muzick-artwork',
|
|
expiration: { maxEntries: 300, maxAgeSeconds: 60 * 60 * 24 * 30 },
|
|
cacheableResponse: { statuses: [0, 200] },
|
|
},
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
],
|
|
test: {
|
|
environment: 'jsdom',
|
|
setupFiles: './src/test/setup.ts',
|
|
clearMocks: true,
|
|
},
|
|
server: {
|
|
proxy: {
|
|
// ponytail: the deployed nginx injects the Authorization header, so dev can
|
|
// point at it (DEV_API_TARGET=http://localhost:5174) instead of a bare backend.
|
|
'/api': process.env.DEV_API_TARGET || 'http://localhost:3000',
|
|
}
|
|
},
|
|
build: {
|
|
outDir: 'dist',
|
|
emptyOutDir: true,
|
|
rollupOptions: {
|
|
output: {
|
|
// The framework changes on an npm upgrade, the app changes every
|
|
// deploy. Splitting them means a redeploy re-downloads app code only,
|
|
// which matters here because the service worker precaches the lot.
|
|
manualChunks: {
|
|
vendor: ['react', 'react-dom', '@tanstack/react-router', '@tanstack/react-query'],
|
|
},
|
|
},
|
|
},
|
|
}
|
|
})
|