diff --git a/scripts/pwa-development.ts b/scripts/pwa-development.ts index c308dfc0..a45c539e 100644 --- a/scripts/pwa-development.ts +++ b/scripts/pwa-development.ts @@ -62,6 +62,12 @@ export async function retryMoviePilotIdentityVerification( return false } +/** 删除当前 origin 的全部 Cache Storage;调用方必须先确认该 dev origin 由 MoviePilot Worker 管理。 */ +export async function deleteCurrentOriginCaches(cacheStorage: Pick): Promise { + const cacheNames = await cacheStorage.keys() + await Promise.all(cacheNames.map(cacheName => cacheStorage.delete(cacheName))) +} + /** 清理完成后只允许返回当前 origin,避免开发中间页形成开放重定向。 */ export function resolveDevCleanupReturnUrl(requested: string | null, origin: string): URL { if (!requested) return new URL('/', origin) @@ -78,6 +84,7 @@ export function createDevServiceWorkerCleanupPlugin(): Plugin { const identityTimeoutMs = JSON.stringify(moviePilotIdentityTimeoutMs) const identityAttempts = JSON.stringify(moviePilotIdentityAttempts) const retryIdentityVerification = retryMoviePilotIdentityVerification.toString() + const deleteOriginCaches = deleteCurrentOriginCaches.toString() const entryScriptUrl = JSON.stringify(devEntryScriptUrl) const cleanupPath = JSON.stringify(DEV_SW_CLEANUP_PATH) const cleanupAttemptKeyPrefix = JSON.stringify('moviepilot:dev-sw-cleanup') @@ -85,6 +92,7 @@ export function createDevServiceWorkerCleanupPlugin(): Plugin { const redirectScript = ` (() => { const entryScriptUrl = ${entryScriptUrl} + const deleteCurrentOriginCaches = ${deleteOriginCaches} let appStarted = false const startApp = () => { if (appStarted) return @@ -150,8 +158,15 @@ export function createDevServiceWorkerCleanupPlugin(): Plugin { // unregister 不会立即解除当前 document 的 controller;应用模块加载前需再导航一次以脱离旧 Worker。 if (cleanupState === 'complete') { - sessionStorage.removeItem(cleanupAttemptKey) - location.reload() + void (async () => { + // 旧 Worker 可能在注销后的首次导航中重新创建缓存;脱离控制后再清理一次。 + if ('caches' in window) await deleteCurrentOriginCaches(caches) + sessionStorage.removeItem(cleanupAttemptKey) + location.reload() + })().catch(error => { + console.error('[PWA] Failed to finish stale development cache cleanup', error) + document.body.textContent = 'Failed to finish stale development cache cleanup. Reload to retry.' + }) return } @@ -180,6 +195,7 @@ export function createDevServiceWorkerCleanupPlugin(): Plugin { const identityTimeoutMs = ${identityTimeoutMs} const identityAttempts = ${identityAttempts} const retryIdentityVerification = ${retryIdentityVerification} + const deleteCurrentOriginCaches = ${deleteOriginCaches} const appScope = new URL('./', location.href) const cleanupAttemptKey = ${cleanupAttemptKeyPrefix} + ':' + encodeURIComponent(appScope.pathname) const resolveReturnUrl = () => { @@ -227,6 +243,9 @@ export function createDevServiceWorkerCleanupPlugin(): Plugin { if (!managedRegistrations.length) throw new Error('MoviePilot Service Worker identity verification failed') await Promise.allSettled(managedRegistrations.map(registration => registration.unregister())) + // Vite dev server 使用独立 origin;仅在确认 MoviePilot Worker 后清除其遗留模块响应。 + if ('caches' in window) await deleteCurrentOriginCaches(caches) + // 回跳入口后由 head-prepend 脚本完成第二次导航,避免同一 client 继续复用旧模块响应。 sessionStorage.setItem(cleanupAttemptKey, 'complete') location.replace(returnUrl.href) diff --git a/src/composables/__tests__/useVersionChecker.spec.ts b/src/composables/__tests__/useVersionChecker.spec.ts new file mode 100644 index 00000000..3c3c6134 --- /dev/null +++ b/src/composables/__tests__/useVersionChecker.spec.ts @@ -0,0 +1,39 @@ +import { useVersionChecker } from '@/composables/useVersionChecker' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ + toastInfo: vi.fn(), +})) + +vi.mock('vue-toastification', () => ({ + useToast: () => ({ info: mocks.toastInfo }), +})) + +describe('useVersionChecker', () => { + beforeEach(() => { + mocks.toastInfo.mockClear() + }) + + it('没有可用 Service Worker 时保留版本不一致的清缓存兜底', async () => { + const { checkVersion } = useVersionChecker() + + await checkVersion('version-that-never-matches-the-build') + + expect(mocks.toastInfo).toHaveBeenCalledOnce() + expect(mocks.toastInfo).toHaveBeenCalledWith( + expect.objectContaining({ + props: expect.objectContaining({ + message: expect.any(String), + onRefresh: expect.any(Function), + refreshText: expect.any(String), + }), + }), + expect.objectContaining({ + closeButton: false, + closeOnClick: false, + draggable: false, + timeout: false, + }), + ) + }) +}) diff --git a/src/composables/useVersionChecker.ts b/src/composables/useVersionChecker.ts index bce801c9..72af4fd5 100644 --- a/src/composables/useVersionChecker.ts +++ b/src/composables/useVersionChecker.ts @@ -3,11 +3,57 @@ import { useToast } from 'vue-toastification' import { Workbox } from 'workbox-window' import i18n from '@/plugins/i18n' import VersionUpdateToast from '@/components/toast/VersionUpdateToast.vue' +import { + createServiceWorkerCoordinator, + resolveServiceWorkerRegistration, + type ServiceWorkerRegistrationConfig, +} from '@/utils/serviceWorkerCoordinator' // 全局状态 const currentVersion = ref(__APP_VERSION__) let isUpdateToastShown = false -let wb: Workbox | null = null + +const serviceWorkerRegistration = + 'serviceWorker' in navigator + ? resolveServiceWorkerRegistration( + import.meta.env.BASE_URL, + document.baseURI, + import.meta.env.DEV, + __PWA_DEVELOPMENT__, + ) + : null + +/** 显示全局唯一的版本更新通知。 */ +function showUpdateNotification(message: string, refreshText?: string, onRefresh?: () => void): void { + if (isUpdateToastShown) return + isUpdateToastShown = true + const component = h(VersionUpdateToast, { + message, + refreshText, + onRefresh, + }) + + useToast().info(component, { + timeout: false, + closeButton: false, + closeOnClick: false, + draggable: false, + }) +} + +const serviceWorkerCoordinator = createServiceWorkerCoordinator({ + registration: serviceWorkerRegistration, + createClient: (registration: ServiceWorkerRegistrationConfig) => + new Workbox(registration.scriptUrl, { + scope: registration.scope, + type: registration.type, + }), + onUpdateActivated: () => { + console.log('[VersionChecker] Service Worker 更新已就绪,等待用户刷新') + showUpdateNotification(i18n.global.t('common.swUpdateReady'), i18n.global.t('common.refresh'), reloadPage) + }, + onError: error => console.error('[VersionChecker] Service Worker 注册失败:', error), +}) /** * 普通刷新页面 @@ -62,16 +108,20 @@ export const clearCacheAndReload = async (): Promise => { const reloadTimer = window.setTimeout(reload, 3000) try { - await Promise.race([ - clearCachesAndServiceWorker(), - new Promise(resolve => window.setTimeout(resolve, 2500)), - ]) + await Promise.race([clearCachesAndServiceWorker(), new Promise(resolve => window.setTimeout(resolve, 2500))]) } finally { window.clearTimeout(reloadTimer) reload() } } +/** + * 初始化应用唯一的 Service Worker coordinator。 + * 普通 dev 不注册;production 与显式 dev:pwa 复用同一个注册 Promise。 + */ +export const initializeServiceWorker = (): Promise => + serviceWorkerCoordinator.initialize() + /** * 版本检查 Composable * @@ -81,49 +131,6 @@ export const clearCacheAndReload = async (): Promise => { * - 显示持久化更新通知 */ export function useVersionChecker() { - const toast = useToast() - - /** - * 显示版本更新通知 - * @param message 通知消息文本 - * @param refreshText 按钮文本,不传则不显示按钮 - * @param onRefresh 按钮点击事件 - */ - const showUpdateNotification = (message: string, refreshText?: string, onRefresh?: () => void): void => { - if (isUpdateToastShown) return - isUpdateToastShown = true - const component = h(VersionUpdateToast, { - message, - refreshText, - onRefresh, - }) - - toast.info(component, { - timeout: false, // 不自动消失 - closeButton: false, - closeOnClick: false, - draggable: false, - }) - } - - // 初始化 Workbox - if (!wb && 'serviceWorker' in navigator) { - wb = new Workbox('/service-worker.js') - - // Service Worker 激活事件 (install -> activate) - wb.addEventListener('activated', event => { - // 只有在更新时才显示通知 - if (event.isUpdate) { - console.log('[VersionChecker] Service Worker 更新已就绪,等待用户刷新') - - showUpdateNotification(i18n.global.t('common.swUpdateReady'), i18n.global.t('common.refresh'), reloadPage) - } - }) - - // 注册 Service Worker - wb.register() - } - /** * 检查版本并在需要时显示更新通知 * @param latestVersion 服务端返回的最新版本号 @@ -141,33 +148,26 @@ export function useVersionChecker() { console.log(`[VersionChecker] 检测到版本不一致: ${currentVersion.value} -> ${latestVersion}`) // 尝试触发 Service Worker 更新检查 - if ('serviceWorker' in navigator && navigator.serviceWorker.controller) { + const registration = await initializeServiceWorker() + if (registration && navigator.serviceWorker.controller) { try { - const registration = await navigator.serviceWorker.getRegistration() - if (registration) { - console.log('[VersionChecker] 触发 Service Worker 更新检查...') + console.log('[VersionChecker] 触发 Service Worker 更新检查...') - // 标记是否发现更新 - let updateFound = false - const onUpdateFound = () => { - updateFound = true - } - - // 监听 updatefound 事件 - registration.addEventListener('updatefound', onUpdateFound, { once: true }) - - // 等待检查完成 - await registration.update() - - // 检查是否有更新正在进行 - // 如果发现更新,或者正在安装/等待中,则直接返回(交由 SW activated 事件处理) - if (updateFound || registration.installing || registration.waiting) { - console.log('[VersionChecker] Service Worker 更新中...') - return - } - - console.log('[VersionChecker] SW 无更新,但版本号不一致,可能是缓存问题') + let updateFound = false + const onUpdateFound = () => { + updateFound = true } + registration.addEventListener('updatefound', onUpdateFound, { once: true }) + + await serviceWorkerCoordinator.update() + + // 更新生命周期由同一个 Workbox 实例继续监听,避免检查和提示使用不同 owner。 + if (updateFound || registration.installing || registration.waiting) { + console.log('[VersionChecker] Service Worker 更新中...') + return + } + + console.log('[VersionChecker] SW 无更新,但版本号不一致,可能是缓存问题') } catch (error) { console.log('[VersionChecker] Service Worker 更新检查失败:', error) // 失败继续向下执行,显示通知 diff --git a/src/main.ts b/src/main.ts index 24ef62c5..800f0dab 100644 --- a/src/main.ts +++ b/src/main.ts @@ -22,6 +22,7 @@ import { emitAgentAssistantToastBubble, type AgentAssistantBubbleVariant, } from '@/utils/agentAssistantBubble' +import { initializeServiceWorker } from '@/composables/useVersionChecker' // 5. 注册自定义组件 import DialogCloseBtn from '@/@core/components/DialogCloseBtn.vue' @@ -178,6 +179,9 @@ app .use(ConfirmDialog) .use(i18n) +// UI 通知依赖安装完成后立即绑定更新监听并启动唯一的 Service Worker 注册。 +void initializeServiceWorker() + app.mount('#app') // 图标全集很大,延后到首屏挂载后的空闲时间加载,避免阻塞登录页首次渲染。 diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 23650204..76e32927 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -2,6 +2,7 @@ declare global { const __APP_VERSION__: string const __BUILD_TIME__: string + const __PWA_DEVELOPMENT__: boolean interface Navigator { /** diff --git a/src/utils/__tests__/serviceWorkerCoordinator.spec.ts b/src/utils/__tests__/serviceWorkerCoordinator.spec.ts new file mode 100644 index 00000000..3f83b999 --- /dev/null +++ b/src/utils/__tests__/serviceWorkerCoordinator.spec.ts @@ -0,0 +1,138 @@ +import { + createServiceWorkerCoordinator, + resolveServiceWorkerRegistration, + type ServiceWorkerClient, +} from '@/utils/serviceWorkerCoordinator' +import { describe, expect, it, vi } from 'vitest' + +describe('Service Worker coordinator', () => { + it('普通 dev 不注册,dev:pwa 使用模块 Worker', () => { + const documentBaseUrl = 'http://localhost:5174/moviepilot/#/dashboard' + + expect(resolveServiceWorkerRegistration('./', documentBaseUrl, true, false)).toBeNull() + expect(resolveServiceWorkerRegistration('./', documentBaseUrl, true, true)).toEqual({ + scriptUrl: 'http://localhost:5174/moviepilot/dev-sw.js?dev-sw', + scope: '/moviepilot/', + type: 'module', + }) + }) + + it('production 按应用 base 注册 classic Worker', () => { + expect( + resolveServiceWorkerRegistration('/moviepilot/', 'https://example.com/ignored/#/dashboard', false, false), + ).toEqual({ + scriptUrl: 'https://example.com/moviepilot/service-worker.js', + scope: '/moviepilot/', + type: 'classic', + }) + }) + + it('监听先于注册,重复初始化和更新复用同一个客户端', async () => { + const events: string[] = [] + let activatedListener: ((event: { isExternal?: boolean; isUpdate?: boolean }) => void) | undefined + const registration = {} as ServiceWorkerRegistration + const client: ServiceWorkerClient = { + addEventListener: vi.fn((_type, listener) => { + events.push('listen') + activatedListener = listener + }), + register: vi.fn(async () => { + events.push('register') + return registration + }), + update: vi.fn(async () => { + events.push('update') + }), + } + const createClient = vi.fn(() => client) + const onUpdateActivated = vi.fn() + const coordinator = createServiceWorkerCoordinator({ + registration: { + scriptUrl: 'https://example.com/moviepilot/service-worker.js', + scope: '/moviepilot/', + type: 'classic', + }, + createClient, + onUpdateActivated, + getController: () => null, + }) + + const firstInitialization = coordinator.initialize() + const secondInitialization = coordinator.initialize() + + expect(firstInitialization).toBe(secondInitialization) + await expect(firstInitialization).resolves.toBe(registration) + expect(createClient).toHaveBeenCalledOnce() + expect(client.register).toHaveBeenCalledOnce() + expect(events).toEqual(['listen', 'register']) + + activatedListener?.({ isUpdate: false }) + expect(onUpdateActivated).not.toHaveBeenCalled() + activatedListener?.({ isExternal: true }) + expect(onUpdateActivated).toHaveBeenCalledOnce() + activatedListener?.({ isUpdate: true }) + expect(onUpdateActivated).toHaveBeenCalledOnce() + + await coordinator.update() + expect(createClient).toHaveBeenCalledOnce() + expect(client.register).toHaveBeenCalledOnce() + expect(client.update).toHaveBeenCalledOnce() + expect(events).toEqual(['listen', 'register', 'update']) + }) + + it('注册期间快速激活时通过 controller 变化补发一次更新通知', async () => { + const previousController = {} as ServiceWorker + const updatedWorker = {} as ServiceWorker + let activatedListener: ((event: { isExternal?: boolean; isUpdate?: boolean }) => void) | undefined + const client: ServiceWorkerClient = { + addEventListener: vi.fn((_type, listener) => { + activatedListener = listener + }), + register: vi.fn(async () => ({ active: updatedWorker }) as ServiceWorkerRegistration), + update: vi.fn(), + } + const onUpdateActivated = vi.fn() + const coordinator = createServiceWorkerCoordinator({ + registration: { + scriptUrl: 'https://example.com/service-worker.js', + scope: '/', + type: 'classic', + }, + createClient: () => client, + onUpdateActivated, + getController: () => previousController, + }) + + await coordinator.initialize() + activatedListener?.({ isUpdate: true }) + + expect(onUpdateActivated).toHaveBeenCalledOnce() + }) + + it('注册失败时只记录一次且不阻断应用启动', async () => { + const error = new Error('register failed') + const onError = vi.fn() + const client: ServiceWorkerClient = { + addEventListener: vi.fn(), + register: vi.fn().mockRejectedValue(error), + update: vi.fn(), + } + const coordinator = createServiceWorkerCoordinator({ + registration: { + scriptUrl: 'https://example.com/service-worker.js', + scope: '/', + type: 'classic', + }, + createClient: () => client, + onUpdateActivated: vi.fn(), + getController: () => null, + onError, + }) + + await expect(coordinator.initialize()).resolves.toBeUndefined() + await expect(coordinator.initialize()).resolves.toBeUndefined() + expect(client.register).toHaveBeenCalledOnce() + expect(onError).toHaveBeenCalledOnce() + expect(onError).toHaveBeenCalledWith(error) + }) +}) diff --git a/src/utils/serviceWorkerCoordinator.ts b/src/utils/serviceWorkerCoordinator.ts new file mode 100644 index 00000000..3a7e08c7 --- /dev/null +++ b/src/utils/serviceWorkerCoordinator.ts @@ -0,0 +1,104 @@ +/** Service Worker 在当前运行模式下的注册参数。 */ +export interface ServiceWorkerRegistrationConfig { + /** Worker 脚本的同源绝对地址。 */ + scriptUrl: string + /** Worker 允许控制的应用路径。 */ + scope: string + /** development Worker 使用 ESM,production Worker 保持 classic。 */ + type: WorkerType +} + +/** Workbox coordinator 所需的最小客户端契约。 */ +export interface ServiceWorkerClient { + addEventListener(type: 'activated', listener: (event: { isExternal?: boolean; isUpdate?: boolean }) => void): void + register(): Promise + update(): Promise +} + +interface ServiceWorkerCoordinatorOptions { + /** 普通 dev 返回 null;production 与显式 dev:pwa 返回实际注册参数。 */ + registration: ServiceWorkerRegistrationConfig | null + /** 创建 Workbox 客户端,由 coordinator 保证全生命周期只调用一次。 */ + createClient: (registration: ServiceWorkerRegistrationConfig) => ServiceWorkerClient + /** 已有 Worker 完成升级激活时通知 UI。 */ + onUpdateActivated: () => void + /** 返回当前页面 controller,用于补偿注册期间完成的快速激活。 */ + getController?: () => ServiceWorker | null + /** 注册失败不阻断应用启动,但必须保留诊断信息。 */ + onError?: (error: unknown) => void +} + +/** + * 根据 Vite base 和运行模式解析唯一的 Worker URL、scope 与脚本类型。 + */ +export function resolveServiceWorkerRegistration( + baseUrl: string, + documentBaseUrl: string, + isDevelopment: boolean, + pwaDevelopmentEnabled: boolean, +): ServiceWorkerRegistrationConfig | null { + if (isDevelopment && !pwaDevelopmentEnabled) return null + + const appBaseUrl = new URL(baseUrl, documentBaseUrl) + const workerScript = isDevelopment ? 'dev-sw.js?dev-sw' : 'service-worker.js' + + return { + scriptUrl: new URL(workerScript, appBaseUrl).href, + scope: appBaseUrl.pathname, + type: isDevelopment ? 'module' : 'classic', + } +} + +/** + * 创建单 owner 的注册协调器;监听器先绑定,随后只执行一次 register。 + */ +export function createServiceWorkerCoordinator(options: ServiceWorkerCoordinatorOptions) { + let client: ServiceWorkerClient | null = null + let registrationPromise: Promise | null = null + let updateNotified = false + + const notifyUpdateActivated = (): void => { + if (updateNotified) return + updateNotified = true + options.onUpdateActivated() + } + + const initialize = (): Promise => { + if (!options.registration) return Promise.resolve(undefined) + if (registrationPromise) return registrationPromise + + const controllerBeforeRegistration = options.getController + ? options.getController() + : navigator.serviceWorker.controller + client = options.createClient(options.registration) + client.addEventListener('activated', event => { + // 其他页签或注册 60 秒后发现的更新会被 Workbox 标为 external,仍代表当前页面需要刷新。 + if (event.isUpdate || event.isExternal) notifyUpdateActivated() + }) + registrationPromise = client + .register() + .then(registration => { + // skipWaiting + clients.claim 可能在 Workbox 接上底层监听前完成;active 已变化时仍需通知用户刷新。 + if (controllerBeforeRegistration && registration?.active !== controllerBeforeRegistration) { + notifyUpdateActivated() + } + return registration + }) + .catch(error => { + options.onError?.(error) + return undefined + }) + + return registrationPromise + } + + const update = async (): Promise => { + await initialize() + await client?.update() + } + + return { + initialize, + update, + } +} diff --git a/tests/config/pwa-development.spec.ts b/tests/config/pwa-development.spec.ts index 8ae4de49..1caa37ba 100644 --- a/tests/config/pwa-development.spec.ts +++ b/tests/config/pwa-development.spec.ts @@ -2,6 +2,7 @@ import type { IndexHtmlTransformContext, IndexHtmlTransformResult, ViteDevServer import { describe, expect, it, vi } from 'vitest' import { createDevServiceWorkerCleanupPlugin, + deleteCurrentOriginCaches, DEV_SW_CLEANUP_PATH, isManagedServiceWorkerRegistration, isMoviePilotServiceWorkerIdentityResponse, @@ -67,6 +68,19 @@ describe('PWA 开发模式', () => { expect(alwaysFails).toHaveBeenCalledTimes(2) }) + it('删除当前 dev origin 的全部 Cache Storage', async () => { + const keys = vi.fn().mockResolvedValue(['precache', 'static-resources', 'api-cache']) + const deleteCache = vi.fn().mockResolvedValue(true) + + await deleteCurrentOriginCaches({ keys, delete: deleteCache }) + + expect(keys).toHaveBeenCalledOnce() + expect(deleteCache).toHaveBeenCalledTimes(3) + expect(deleteCache).toHaveBeenNthCalledWith(1, 'precache') + expect(deleteCache).toHaveBeenNthCalledWith(2, 'static-resources') + expect(deleteCache).toHaveBeenNthCalledWith(3, 'api-cache') + }) + it('清理完成后只返回当前 origin', () => { const origin = 'http://localhost:5173' @@ -112,8 +126,11 @@ describe('PWA 开发模式', () => { expect(scriptContent).toContain("cleanupState === 'complete'") expect(scriptContent).toContain("sessionStorage.setItem(cleanupAttemptKey, 'pending')") expect(scriptContent).toContain('encodeURIComponent(appScope.pathname)') + expect(scriptContent).toContain('deleteCurrentOriginCaches(caches)') expect(scriptContent).toContain('location.reload()') - expect(scriptContent).not.toContain('caches.delete') + expect(scriptContent.indexOf('deleteCurrentOriginCaches(caches)')).toBeLessThan( + scriptContent.indexOf('sessionStorage.removeItem(cleanupAttemptKey)'), + ) }) it('开发入口标签缺失时立即失败,避免普通 dev 静默白屏', () => { @@ -158,8 +175,17 @@ describe('PWA 开发模式', () => { expect(response.end).toHaveBeenCalledWith( expect.stringContaining("sessionStorage.setItem(cleanupAttemptKey, 'complete')"), ) - expect(response.end).not.toHaveBeenCalledWith(expect.stringContaining('caches.delete')) + expect(response.end).toHaveBeenCalledWith(expect.stringContaining('deleteCurrentOriginCaches(caches)')) expect(response.end).not.toHaveBeenCalledWith(expect.stringContaining('localStorage.clear')) + + const cleanupDocument = vi.mocked(response.end).mock.calls[0]?.[0] + if (typeof cleanupDocument !== 'string') throw new TypeError('Expected cleanup document') + expect(cleanupDocument.indexOf("sessionStorage.getItem(cleanupAttemptKey) !== 'pending'")).toBeLessThan( + cleanupDocument.indexOf('deleteCurrentOriginCaches(caches)'), + ) + expect(cleanupDocument.indexOf('if (!managedRegistrations.length) throw new Error')).toBeLessThan( + cleanupDocument.indexOf('deleteCurrentOriginCaches(caches)'), + ) }) }) diff --git a/vite.config.ts b/vite.config.ts index ce7f1b7b..13183c14 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -70,8 +70,7 @@ export default defineConfig(({ command, mode, isPreview }) => ({ }), !isTestMode(mode) && VitePWA({ - injectRegister: 'script', - registerType: 'autoUpdate', + injectRegister: false, strategies: 'injectManifest', srcDir: 'src', filename: 'service-worker.ts', @@ -211,6 +210,7 @@ export default defineConfig(({ command, mode, isPreview }) => ({ 'process.env': {}, '__APP_VERSION__': JSON.stringify(`v${packageJson.version}`), '__BUILD_TIME__': JSON.stringify(buildTime), + '__PWA_DEVELOPMENT__': JSON.stringify(isPwaDevelopmentEnabled(mode, process.env.npm_lifecycle_event)), }, resolve: { alias: {