Repository navigation
Expand file tree
/
Copy pathvite.config.js
More file actions
157 lines (148 loc) · 6.33 KB
/
Copy pathvite.config.js
File metadata and controls
157 lines (148 loc) · 6.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
import { rm } from 'node:fs/promises'
import { execSync } from 'node:child_process'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { VitePWA } from 'vite-plugin-pwa'
// The public/samples/*.wav tones are dev-only seed fixtures (see src/lib/seed.js,
// gated behind import.meta.env.DEV). Because they live in public/ they'd be
// copied verbatim into the build; strip them from the production output so
// ~2.7MB of audio no user ever plays doesn't ship.
const stripDevSamplesPlugin = () => ({
name: 'melody-strip-dev-samples',
apply: 'build',
closeBundle: async () => {
await rm('dist/samples', { recursive: true, force: true })
},
})
// Content-Security-Policy (defense-in-depth). Injected into index.html for
// PRODUCTION BUILDS ONLY — dev/HMR needs inline scripts + eval, which this would
// block. Sources: app assets are same-origin ('self'); thumbnails come from
// YouTube's image CDNs; JSON is fetched from noembed + lrclib; imported audio
// and artwork play from blob: URLs; React sets inline style attributes
// ('unsafe-inline' for style only, never script). Podcasts widen img, media
// and connect to any https: host, since feeds, cover art and episode audio
// live wherever each show is hosted.
const CSP = [
"default-src 'self'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob: https:",
"media-src 'self' blob: https:",
"connect-src 'self' https:",
"font-src 'self'",
"manifest-src 'self'",
"worker-src 'self'",
"object-src 'none'",
"base-uri 'none'",
].join('; ')
const cspPlugin = () => ({
name: 'melody-csp',
transformIndexHtml: {
order: 'post',
// Inject as the FIRST head child so the policy governs every subresource
// (the app bundle, CSS, fonts) — a meta CSP doesn't apply to requests made
// before it's parsed.
handler: (html) =>
html.replace('<head>', `<head>\n <meta http-equiv="Content-Security-Policy" content="${CSP}" />`),
},
})
// NOTE on `base`: production is served from a SUBPATH, so the built asset URLs
// must be prefixed with it or the deployed app loads a blank white screen (the
// JS/CSS 404). The live deploy is Firebase via `npm run deploy:sparky`, hosted
// at https://sparkylab.web.app/melody/ — so '/melody/' (lowercase) is correct.
// Local dev uses '/'.
//
// ⚠️ If you ever switch to GitHub Pages instead, the path is case-SENSITIVE and
// preserves the repo name: https://enesyilmazcode.github.io/Melody/ — you'd
// need base '/Melody/' (capital M), or rename the repo to lowercase. Don't
// change this to '/Melody/' while deploying to Firebase — that would break it.
// Build stamp shown by the hidden version readout (long-press the tab bar).
// Frozen at build time, so on the phone it identifies exactly which deploy the
// service worker has actually activated — the way to confirm auto-update worked
// without any visible UI.
const commit = (() => {
try {
return execSync('git rev-parse --short HEAD').toString().trim()
} catch {
return 'unknown'
}
})()
export default defineConfig(({ command }) => ({
base: command === 'build' ? '/melody/' : '/',
define: {
__BUILD_INFO__: JSON.stringify({ commit, builtAt: new Date().toISOString() }),
},
plugins: [
// CSP only in the built HTML — injecting it in dev would break Vite HMR.
command === 'build' && cspPlugin(),
stripDevSamplesPlugin(), // apply:'build' gates it to production
react(),
VitePWA({
// 'autoUpdate' = the new service worker activates and reloads the page as
// soon as a new build is detected. This directly solves your §7 worry
// ("works offline but I don't want stale builds"): the app shell is cached
// for offline use, but a fresh deploy supersedes the cache automatically.
registerType: 'autoUpdate',
// Files copied as-is into the build (favicon, apple-touch-icon, etc.).
includeAssets: ['favicon.ico', 'apple-touch-icon-180x180.png', 'logo.svg'],
// This object becomes the generated manifest.json. iOS reads `name`,
// `icons`, `theme_color`, and `display: standalone` to render a real
// fullscreen app (no Safari chrome) when you Add to Home Screen.
manifest: {
name: 'Melody',
short_name: 'Melody',
description: 'Personal local audio player',
theme_color: '#15120e',
background_color: '#15120e',
display: 'standalone',
orientation: 'portrait',
// Relative scope/start so it works under the /melody/ subpath.
scope: '.',
start_url: '.',
icons: [
{ src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png' },
{ src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png' },
{
src: 'maskable-icon-512x512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'maskable',
},
],
},
// Workbox config: precache the app shell so Melody opens with no network
// (the whole point — bad subway wifi). Audio files are NOT precached here;
// they live in Files and load on demand (Phase 3/6).
workbox: {
globPatterns: ['**/*.{js,css,html,svg,png,ico,woff2}'],
cleanupOutdatedCaches: true,
// Cache YouTube cover thumbnails after first view so artwork shows
// offline on the subway.
runtimeCaching: [
{
urlPattern: /^https:\/\/i\.ytimg\.com\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'yt-thumbs',
expiration: { maxEntries: 300, maxAgeSeconds: 60 * 60 * 24 * 90 },
},
},
// Podcast cover art, from whatever host each show uses. Opaque
// (status 0) responses are fine to cache for an <img>.
{
urlPattern: ({ request, url }) => request.destination === 'image' && url.origin !== self.location.origin,
handler: 'CacheFirst',
options: {
cacheName: 'podcast-art',
cacheableResponse: { statuses: [0, 200] },
expiration: { maxEntries: 300, maxAgeSeconds: 60 * 60 * 24 * 90 },
},
},
],
},
// Lets the service worker run on `npm run dev` so you can test
// installability/offline locally before deploying.
devOptions: { enabled: true },
}),
],
}))