115 lines
5.3 KiB
JavaScript
115 lines
5.3 KiB
JavaScript
import fs from 'node:fs'
|
|
import { defineConfig } from 'vite'
|
|
import vue from '@vitejs/plugin-vue'
|
|
import tailwindcss from '@tailwindcss/vite'
|
|
import { VitePWA } from 'vite-plugin-pwa'
|
|
import { fileURLToPath, URL } from 'node:url'
|
|
|
|
// Opt-in HTTPS for the dev server, off by default so the usual
|
|
// http://localhost:5173 workflow is unchanged. Only needed if you want to test
|
|
// the installed PWA from a phone over the LAN *without* using
|
|
// chrome://flags/#unsafely-treat-insecure-origin-as-secure -- and note Chrome
|
|
// needs the mkcert root CA installed on the device for the origin to be
|
|
// service-worker eligible; clicking through a cert warning is not enough.
|
|
// DEV_HTTPS=1 pnpm dev
|
|
const certDir = fileURLToPath(new URL('./certs', import.meta.url))
|
|
const keyPath = `${certDir}/192.168.1.164+4-key.pem`
|
|
const certPath = `${certDir}/192.168.1.164+4.pem`
|
|
const useHttps =
|
|
process.env.DEV_HTTPS === '1' && fs.existsSync(keyPath) && fs.existsSync(certPath)
|
|
|
|
export default defineConfig({
|
|
plugins: [
|
|
vue({
|
|
template: {
|
|
transformAssetUrls: { includeAbsolute: false },
|
|
},
|
|
}),
|
|
tailwindcss(),
|
|
VitePWA({
|
|
// Updates are user-driven via the prompt in src/registerServiceWorker.js
|
|
registerType: 'prompt',
|
|
injectRegister: false,
|
|
// Lands at dist/sw.js -- root of the served directory, so the worker gets
|
|
// root scope without needing a Service-Worker-Allowed header.
|
|
filename: 'sw.js',
|
|
// We ship public/manifest.json by hand rather than generating one.
|
|
manifest: false,
|
|
workbox: {
|
|
//
|
|
// SECURITY INVARIANT -- do not relax without thinking it through.
|
|
//
|
|
// Solid Scribe is end-to-end encrypted. The service worker precaches
|
|
// ONLY static build output and is network-only for /api/*. Caching an
|
|
// API response would write decrypted note content into Cache Storage on
|
|
// disk, where it survives logout and sits outside the encryption model.
|
|
//
|
|
// Structurally this holds because globPatterns only ever reads dist/,
|
|
// and user content is served by Express from staticFiles/ at
|
|
// /api/static -- which never enters dist. The NetworkOnly rule below is
|
|
// belt-and-braces: it documents the invariant and stops a future
|
|
// runtimeCaching entry from silently widening the net.
|
|
//
|
|
// woff2 is NOT in Workbox's default globPatterns. Without it the Roboto
|
|
// files are skipped and the offline shell renders in a fallback face.
|
|
globPatterns: ['**/*.{js,css,html,woff2,png,svg,ico,json}'],
|
|
// Vendor chunks (fomantic, chart.js) exceed the 2MiB default. An
|
|
// oversized file is silently OMITTED from the precache, which half
|
|
// breaks the offline shell -- watch the build output for a warning.
|
|
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
|
|
navigateFallback: '/index.html',
|
|
// Matters for top-level navigations to /api/static/<file> (clicking an
|
|
// attachment link) -- without this the worker answers with index.html.
|
|
navigateFallbackDenylist: [/^\/api\//, /^\/socket/],
|
|
cleanupOutdatedCaches: true,
|
|
// Let the user decide when to swap assets, rather than doing it
|
|
// mid-session underneath them.
|
|
clientsClaim: false,
|
|
skipWaiting: false,
|
|
//
|
|
// runtimeCaching is deliberately EMPTY. Do not add an entry for /api/*
|
|
// or /socket -- not even NetworkOnly.
|
|
//
|
|
// There used to be a NetworkOnly route for both, as belt-and-braces for
|
|
// the invariant above. It was worse than useless. registerRoute() makes
|
|
// the worker call event.respondWith() for every matching request, and
|
|
// Socket.IO's long-poll requests are aborted as a matter of course --
|
|
// on reconnect, on transport upgrade, and whenever Android backgrounds
|
|
// the page. An aborted fetch inside NetworkOnly throws Workbox's
|
|
// `no-response`, the respondWith promise rejects, and Socket.IO sees a
|
|
// dead transport and reconnects. The installed PWA was dropping its
|
|
// live note sync on a loop and filling the logs with
|
|
// "FetchEvent ... resulted in a network error response".
|
|
//
|
|
// With no route registered the worker never intercepts these at all and
|
|
// the browser handles them natively, exactly as in a normal tab.
|
|
//
|
|
// Nothing is lost on the security side: the invariant is enforced by
|
|
// globPatterns only ever reading dist/, and by there being no *caching*
|
|
// strategy here. NetworkOnly enforced nothing -- it only inserted the
|
|
// worker into the hot path of every poll.
|
|
//
|
|
},
|
|
// Flip to true temporarily to exercise the worker on the dev server.
|
|
// Remember to unregister it afterwards -- a lingering dev service worker
|
|
// is a classic "why is my phone showing an ancient build".
|
|
devOptions: { enabled: false, type: 'module' },
|
|
}),
|
|
],
|
|
resolve: {
|
|
alias: {
|
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
|
},
|
|
},
|
|
server: {
|
|
host: '0.0.0.0',
|
|
...(useHttps
|
|
? { https: { key: fs.readFileSync(keyPath), cert: fs.readFileSync(certPath) } }
|
|
: {}),
|
|
proxy: {
|
|
'/api': 'http://localhost:3000',
|
|
'/socket': { target: 'http://localhost:3001', ws: true },
|
|
},
|
|
},
|
|
})
|