fix(dev): self-heal stale service workers + standardize PWA meta
Two related dev-quality fixes that compose to remove a footgun.
1. Stale service worker self-cleanup (src/lib/dev-sw-cleanup.ts)
Even with VitePWA's devOptions.enabled now false (commit 613a925),
service workers registered during earlier dev sessions linger in
the browser and intercept navigations, often serving cached bundles
from the broken-config period. Manifested as: castle/chat/wallet
not redirecting to /login despite the new auth guard, forum/market
showing "Failed to Start: Cannot read properties of undefined" for
modules that aren't even in their standalone config, hub redirecting
to /market on refresh.
The new helper runs at app boot in dev only:
- enumerates navigator.serviceWorker.getRegistrations()
- unregisters every one of them
- clears caches.keys()
- reloads once (gated by sessionStorage to avoid loops)
In production builds it's a no-op — the legitimate SW registered
by virtual:pwa-register survives.
Wired into all 8 main.ts entry points (hub + 7 standalones).
2. Apple-mobile-web-app-capable deprecation (.html)
Browsers now warn that <meta name="apple-mobile-web-app-capable">
should be paired with the standardized <meta name="mobile-web-app-capable">.
Adding the standardized tag alongside (kept the apple variant for
older iOS Safari) on all 8 HTML entry points.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
613a925e45
commit
3ec66151a7
17 changed files with 75 additions and 0 deletions
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<!-- <meta name="theme-color" content="#ffffff"> -->
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { startApp } from './app'
|
||||
import { registerSW } from 'virtual:pwa-register'
|
||||
import { cleanupStaleDevServiceWorkers } from '@/lib/dev-sw-cleanup'
|
||||
import 'vue-sonner/style.css'
|
||||
|
||||
cleanupStaleDevServiceWorkers()
|
||||
|
||||
// PWA service worker with periodic updates
|
||||
const intervalMS = 60 * 60 * 1000 // 1 hour
|
||||
registerSW({
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { startApp } from './app'
|
||||
import { registerSW } from 'virtual:pwa-register'
|
||||
import { cleanupStaleDevServiceWorkers } from '@/lib/dev-sw-cleanup'
|
||||
import 'vue-sonner/style.css'
|
||||
|
||||
cleanupStaleDevServiceWorkers()
|
||||
|
||||
// PWA service worker with periodic updates
|
||||
const intervalMS = 60 * 60 * 1000 // 1 hour
|
||||
registerSW({
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { startApp } from './app'
|
||||
import { registerSW } from 'virtual:pwa-register'
|
||||
import { cleanupStaleDevServiceWorkers } from '@/lib/dev-sw-cleanup'
|
||||
import 'vue-sonner/style.css'
|
||||
|
||||
cleanupStaleDevServiceWorkers()
|
||||
|
||||
const intervalMS = 60 * 60 * 1000
|
||||
registerSW({
|
||||
onRegistered(r) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { startApp } from './app'
|
||||
import { registerSW } from 'virtual:pwa-register'
|
||||
import { cleanupStaleDevServiceWorkers } from '@/lib/dev-sw-cleanup'
|
||||
import 'vue-sonner/style.css'
|
||||
|
||||
cleanupStaleDevServiceWorkers()
|
||||
|
||||
const intervalMS = 60 * 60 * 1000
|
||||
registerSW({
|
||||
onRegistered(r) {
|
||||
|
|
|
|||
42
src/lib/dev-sw-cleanup.ts
Normal file
42
src/lib/dev-sw-cleanup.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* Unregister any service worker that was registered on this origin during
|
||||
* a previous dev session (when VitePWA's devOptions.enabled was true).
|
||||
*
|
||||
* Once devOptions.enabled was turned off, Vite stopped registering SWs in
|
||||
* dev — but the browser keeps the previously-registered SWs alive across
|
||||
* server restarts. They then intercept navigation and serve cached, often
|
||||
* stale, bundles. This call clears them out at app boot.
|
||||
*
|
||||
* Production builds skip this entirely so the legitimate SW from
|
||||
* `registerSW()` survives.
|
||||
*/
|
||||
export async function cleanupStaleDevServiceWorkers(): Promise<void> {
|
||||
if (!import.meta.env.DEV) return
|
||||
if (!('serviceWorker' in navigator)) return
|
||||
|
||||
try {
|
||||
const regs = await navigator.serviceWorker.getRegistrations()
|
||||
if (regs.length === 0) return
|
||||
|
||||
console.warn(
|
||||
`[dev-sw-cleanup] Unregistering ${regs.length} stale service worker(s) from a previous dev session.`
|
||||
)
|
||||
await Promise.all(regs.map(r => r.unregister()))
|
||||
|
||||
// Also clear any cache the dev SW left behind.
|
||||
if ('caches' in window) {
|
||||
const keys = await caches.keys()
|
||||
await Promise.all(keys.map(k => caches.delete(k)))
|
||||
}
|
||||
|
||||
// Reload once so the next request hits the network instead of the
|
||||
// about-to-be-removed SW. Guard with a sessionStorage flag so we don't
|
||||
// loop on browsers that take an extra tick to release the controller.
|
||||
if (!sessionStorage.getItem('dev-sw-cleanup-reloaded')) {
|
||||
sessionStorage.setItem('dev-sw-cleanup-reloaded', '1')
|
||||
window.location.reload()
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[dev-sw-cleanup] failed to unregister:', err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,12 @@
|
|||
// New modular application entry point
|
||||
import { startApp } from './app'
|
||||
import { registerSW } from 'virtual:pwa-register'
|
||||
import { cleanupStaleDevServiceWorkers } from '@/lib/dev-sw-cleanup'
|
||||
import 'vue-sonner/style.css'
|
||||
|
||||
// Clean up any leftover dev-mode service workers from a previous session
|
||||
cleanupStaleDevServiceWorkers()
|
||||
|
||||
// Simple periodic service worker updates
|
||||
const intervalMS = 60 * 60 * 1000 // 1 hour
|
||||
registerSW({
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { startApp } from './app'
|
||||
import { registerSW } from 'virtual:pwa-register'
|
||||
import { cleanupStaleDevServiceWorkers } from '@/lib/dev-sw-cleanup'
|
||||
import 'vue-sonner/style.css'
|
||||
|
||||
cleanupStaleDevServiceWorkers()
|
||||
|
||||
const intervalMS = 60 * 60 * 1000
|
||||
registerSW({
|
||||
onRegistered(r) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { startApp } from './app'
|
||||
import { registerSW } from 'virtual:pwa-register'
|
||||
import { cleanupStaleDevServiceWorkers } from '@/lib/dev-sw-cleanup'
|
||||
import 'vue-sonner/style.css'
|
||||
|
||||
cleanupStaleDevServiceWorkers()
|
||||
|
||||
const intervalMS = 60 * 60 * 1000
|
||||
registerSW({
|
||||
onRegistered(r) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { startApp } from './app'
|
||||
import { registerSW } from 'virtual:pwa-register'
|
||||
import { cleanupStaleDevServiceWorkers } from '@/lib/dev-sw-cleanup'
|
||||
import 'vue-sonner/style.css'
|
||||
|
||||
cleanupStaleDevServiceWorkers()
|
||||
|
||||
const intervalMS = 60 * 60 * 1000
|
||||
registerSW({
|
||||
onRegistered(r) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue