From ac1ead1efe55a81ac5747abd4b4de31de47a7fcc Mon Sep 17 00:00:00 2001 From: maiieul Date: Wed, 27 Aug 2025 09:40:36 +0200 Subject: [PATCH 1/6] fix: custom service-worker.ts code is also being unregistered --- .../generate-service-worker.ts | 40 ++++++++++++++----- .../qwik-city/src/buildtime/vite/plugin.ts | 24 ++++++++++- .../qwik-city/src/runtime/src/sw-register.ts | 23 ++--------- 3 files changed, 58 insertions(+), 29 deletions(-) diff --git a/packages/qwik-city/src/buildtime/runtime-generation/generate-service-worker.ts b/packages/qwik-city/src/buildtime/runtime-generation/generate-service-worker.ts index 1f9209a43ce..2fa13ca8e9e 100644 --- a/packages/qwik-city/src/buildtime/runtime-generation/generate-service-worker.ts +++ b/packages/qwik-city/src/buildtime/runtime-generation/generate-service-worker.ts @@ -1,31 +1,53 @@ import type { BuildContext } from '../types'; -export function generateServiceWorkerRegister(ctx: BuildContext, swRegister: string) { +export function generateServiceWorkerRegister( + ctx: BuildContext, + swRegister: string, + didNotAddCustomCode: boolean +) { let swReg: string; + let swUrl = '/service-worker.js'; - if (ctx.isDevServer) { + // Also unregister if the developer did not add custom code to the service worker since Qwik 1.14.0 and above now use modulepreload by default + if (ctx.isDevServer || didNotAddCustomCode) { swReg = SW_UNREGISTER; } else { swReg = swRegister; - let swUrl = '/service-worker.js'; if (ctx.serviceWorkers.length > 0) { const sw = ctx.serviceWorkers.sort((a, b) => a.chunkFileName.length < b.chunkFileName.length ? -1 : 1 )[0]; swUrl = ctx.opts.basePathname + sw.chunkFileName; } - - swReg = swReg.replace('__url', swUrl); } + swReg = swReg.replace('__url', swUrl); return `export default ${JSON.stringify(swReg)};`; } const SW_UNREGISTER = ` -navigator.serviceWorker?.getRegistrations().then((regs) => { - for (const reg of regs) { - reg.unregister(); +(() => { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.getRegistrations().then((regs) => { + for (const reg of regs) { + const url = '__url'.split('/').pop(); + if (reg.active?.scriptURL.endsWith(url || 'service-worker.js')) { + reg.unregister().catch(console.error); + } + } + }); + } + if ('caches' in window) { + caches + .keys() + .then((names) => { + const cacheName = names.find((name) => name.startsWith('QwikBuild')); + if (cacheName) { + caches.delete(cacheName).catch(console.error); + } + }) + .catch(console.error); } -}); +})(); `; diff --git a/packages/qwik-city/src/buildtime/vite/plugin.ts b/packages/qwik-city/src/buildtime/vite/plugin.ts index 880e5d22a9c..b233b09ea82 100644 --- a/packages/qwik-city/src/buildtime/vite/plugin.ts +++ b/packages/qwik-city/src/buildtime/vite/plugin.ts @@ -186,8 +186,30 @@ function qwikCityPlugin(userOpts?: QwikCityVitePluginOptions): any { } if (isSwRegister) { + // Detect if the developer added custom code in their service-worker.ts file + let didNotAddCustomCode = false; + const clientOutDir = qwikPlugin!.api.getClientOutDir(); + const basePathRelDir = api.getBasePathname().replace(/^\/|\/$/, ''); + const clientOutBaseDir = join(clientOutDir, basePathRelDir); + for (const swEntry of ctx.serviceWorkers) { + try { + const swClientDistPath = join(clientOutBaseDir, swEntry.chunkFileName); + const swCode = await fs.promises.readFile(swClientDistPath, 'utf-8'); + const swCodeTrimmed = swCode.replace(/\s+/g, ''); + if ( + swCodeTrimmed === + 'addEventListener("install",()=>self.skipWaiting());addEventListener("activate",()=>self.clients.claim());' || + swCodeTrimmed === + 'self.addEventListener("install",()=>self.skipWaiting());self.addEventListener("activate",()=>self.clients.claim());' + ) { + didNotAddCustomCode = true; + } + } catch (e) { + console.error('Error reading service worker file', e); + } + } // @qwik-city-sw-register - return generateServiceWorkerRegister(ctx, swRegister); + return generateServiceWorkerRegister(ctx, swRegister, didNotAddCustomCode); } } } diff --git a/packages/qwik-city/src/runtime/src/sw-register.ts b/packages/qwik-city/src/runtime/src/sw-register.ts index 8df878bc83c..a670d0f2944 100644 --- a/packages/qwik-city/src/runtime/src/sw-register.ts +++ b/packages/qwik-city/src/runtime/src/sw-register.ts @@ -2,24 +2,9 @@ (() => { if ('serviceWorker' in navigator) { - navigator.serviceWorker.getRegistrations().then((regs) => { - for (const reg of regs) { - const url = '__url'.split('/').pop(); - if (reg.active?.scriptURL.endsWith(url || 'service-worker.js')) { - reg.unregister().catch(console.error); - } - } - }); - } - if ('caches' in window) { - caches - .keys() - .then((names) => { - const cacheName = names.find((name) => name.startsWith('QwikBuild')); - if (cacheName) { - caches.delete(cacheName).catch(console.error); - } - }) - .catch(console.error); + navigator.serviceWorker.register('__url').catch((e) => console.error(e)); + } else { + // eslint-disable-next-line no-console + console.log('Service worker not supported in this browser.'); } })(); From 04d2d55b5c6b754146e0ba007788c5d4169a0008 Mon Sep 17 00:00:00 2001 From: maiieul Date: Wed, 27 Aug 2025 09:41:42 +0200 Subject: [PATCH 2/6] chore: changeset --- .changeset/thin-horses-bake.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/thin-horses-bake.md diff --git a/.changeset/thin-horses-bake.md b/.changeset/thin-horses-bake.md new file mode 100644 index 00000000000..8830dfbcf8b --- /dev/null +++ b/.changeset/thin-horses-bake.md @@ -0,0 +1,5 @@ +--- +'@builder.io/qwik-city': patch +--- + +patch: FIX: Your service-worker.js won't be unregistered anymore if you added custom logic. From 81f0271d764b46999aa564596b7bdc912fec8a18 Mon Sep 17 00:00:00 2001 From: maiieul Date: Wed, 27 Aug 2025 09:55:36 +0200 Subject: [PATCH 3/6] fix: clientOutDir type issue --- .../qwik-city/src/buildtime/vite/plugin.ts | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/packages/qwik-city/src/buildtime/vite/plugin.ts b/packages/qwik-city/src/buildtime/vite/plugin.ts index b233b09ea82..44c18b7e99b 100644 --- a/packages/qwik-city/src/buildtime/vite/plugin.ts +++ b/packages/qwik-city/src/buildtime/vite/plugin.ts @@ -189,23 +189,25 @@ function qwikCityPlugin(userOpts?: QwikCityVitePluginOptions): any { // Detect if the developer added custom code in their service-worker.ts file let didNotAddCustomCode = false; const clientOutDir = qwikPlugin!.api.getClientOutDir(); - const basePathRelDir = api.getBasePathname().replace(/^\/|\/$/, ''); - const clientOutBaseDir = join(clientOutDir, basePathRelDir); - for (const swEntry of ctx.serviceWorkers) { - try { - const swClientDistPath = join(clientOutBaseDir, swEntry.chunkFileName); - const swCode = await fs.promises.readFile(swClientDistPath, 'utf-8'); - const swCodeTrimmed = swCode.replace(/\s+/g, ''); - if ( - swCodeTrimmed === - 'addEventListener("install",()=>self.skipWaiting());addEventListener("activate",()=>self.clients.claim());' || - swCodeTrimmed === - 'self.addEventListener("install",()=>self.skipWaiting());self.addEventListener("activate",()=>self.clients.claim());' - ) { - didNotAddCustomCode = true; + if (clientOutDir) { + const basePathRelDir = api.getBasePathname().replace(/^\/|\/$/, ''); + const clientOutBaseDir = join(clientOutDir, basePathRelDir); + for (const swEntry of ctx.serviceWorkers) { + try { + const swClientDistPath = join(clientOutBaseDir, swEntry.chunkFileName); + const swCode = await fs.promises.readFile(swClientDistPath, 'utf-8'); + const swCodeTrimmed = swCode.replace(/\s+/g, ''); + if ( + swCodeTrimmed === + 'addEventListener("install",()=>self.skipWaiting());addEventListener("activate",()=>self.clients.claim());' || + swCodeTrimmed === + 'self.addEventListener("install",()=>self.skipWaiting());self.addEventListener("activate",()=>self.clients.claim());' + ) { + didNotAddCustomCode = true; + } + } catch (e) { + console.error('Error reading service worker file', e); } - } catch (e) { - console.error('Error reading service worker file', e); } } // @qwik-city-sw-register From f507f601b147baaea2026fb5b21db02023b2565f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=AFeul?= <45822175+maiieul@users.noreply.github.com> Date: Wed, 27 Aug 2025 11:11:39 +0200 Subject: [PATCH 4/6] Update .changeset/thin-horses-bake.md Co-authored-by: Giorgio Boa <35845425+gioboa@users.noreply.github.com> --- .changeset/thin-horses-bake.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/thin-horses-bake.md b/.changeset/thin-horses-bake.md index 8830dfbcf8b..b542bb3f20a 100644 --- a/.changeset/thin-horses-bake.md +++ b/.changeset/thin-horses-bake.md @@ -2,4 +2,4 @@ '@builder.io/qwik-city': patch --- -patch: FIX: Your service-worker.js won't be unregistered anymore if you added custom logic. +FIX: Your service-worker.js won't be unregistered anymore if you added custom logic to it. From 1333911bca50d60c21929adb9c667220d26fca2b Mon Sep 17 00:00:00 2001 From: maiieul Date: Wed, 27 Aug 2025 14:20:32 +0200 Subject: [PATCH 5/6] fix: also unregister when service-worker.ts is removed and delete cache in any case --- .../generate-service-worker.ts | 62 +++++++++---------- .../qwik-city/src/runtime/src/sw-register.ts | 12 ++++ 2 files changed, 43 insertions(+), 31 deletions(-) diff --git a/packages/qwik-city/src/buildtime/runtime-generation/generate-service-worker.ts b/packages/qwik-city/src/buildtime/runtime-generation/generate-service-worker.ts index 2fa13ca8e9e..ac1a7a8c0f7 100644 --- a/packages/qwik-city/src/buildtime/runtime-generation/generate-service-worker.ts +++ b/packages/qwik-city/src/buildtime/runtime-generation/generate-service-worker.ts @@ -8,18 +8,16 @@ export function generateServiceWorkerRegister( let swReg: string; let swUrl = '/service-worker.js'; - // Also unregister if the developer did not add custom code to the service worker since Qwik 1.14.0 and above now use modulepreload by default - if (ctx.isDevServer || didNotAddCustomCode) { + // Also unregister if the developer removed the service-worker.ts file or did not add custom code to it; since Qwik 1.14.0 and above now use modulepreload by default + if (ctx.isDevServer || didNotAddCustomCode || ctx.serviceWorkers.length === 0) { swReg = SW_UNREGISTER; } else { swReg = swRegister; - if (ctx.serviceWorkers.length > 0) { - const sw = ctx.serviceWorkers.sort((a, b) => - a.chunkFileName.length < b.chunkFileName.length ? -1 : 1 - )[0]; - swUrl = ctx.opts.basePathname + sw.chunkFileName; - } + const sw = ctx.serviceWorkers.sort((a, b) => + a.chunkFileName.length < b.chunkFileName.length ? -1 : 1 + )[0]; + swUrl = ctx.opts.basePathname + sw.chunkFileName; } swReg = swReg.replace('__url', swUrl); @@ -27,27 +25,29 @@ export function generateServiceWorkerRegister( } const SW_UNREGISTER = ` -(() => { - if ('serviceWorker' in navigator) { - navigator.serviceWorker.getRegistrations().then((regs) => { - for (const reg of regs) { - const url = '__url'.split('/').pop(); - if (reg.active?.scriptURL.endsWith(url || 'service-worker.js')) { - reg.unregister().catch(console.error); - } - } - }); - } - if ('caches' in window) { - caches - .keys() - .then((names) => { - const cacheName = names.find((name) => name.startsWith('QwikBuild')); - if (cacheName) { - caches.delete(cacheName).catch(console.error); - } - }) - .catch(console.error); - } -})(); +"serviceWorker"in navigator&&navigator.serviceWorker.getRegistrations().then(r=>{for(const e of r){const c='__url'.split("/").pop();e.active?.scriptURL.endsWith(c||"service-worker.js")&&e.unregister().catch(console.error)}}),"caches"in window&&caches.keys().then(r=>{const e=r.find(c=>c.startsWith("QwikBuild"));e&&caches.delete(e).catch(console.error)}).catch(console.error) `; +// Code in SW_UNREGISTER unregisters the service worker and deletes the cache; it is the minified version of the following: +// (() => { +// if ('serviceWorker' in navigator) { +// navigator.serviceWorker.getRegistrations().then((regs) => { +// for (const reg of regs) { +// const url = '__url'.split('/').pop(); +// if (reg.active?.scriptURL.endsWith(url || 'service-worker.js')) { +// reg.unregister().catch(console.error); +// } +// } +// }); +// } +// if ('caches' in window) { +// caches +// .keys() +// .then((names) => { +// const cacheName = names.find((name) => name.startsWith('QwikBuild')); +// if (cacheName) { +// caches.delete(cacheName).catch(console.error); +// } +// }) +// .catch(console.error); +// } +// })(); diff --git a/packages/qwik-city/src/runtime/src/sw-register.ts b/packages/qwik-city/src/runtime/src/sw-register.ts index a670d0f2944..89b19fecefa 100644 --- a/packages/qwik-city/src/runtime/src/sw-register.ts +++ b/packages/qwik-city/src/runtime/src/sw-register.ts @@ -3,6 +3,18 @@ (() => { if ('serviceWorker' in navigator) { navigator.serviceWorker.register('__url').catch((e) => console.error(e)); + // We need to delete the cache since we are using modulepreload by default in qwik 1.14 and above + if ('caches' in window) { + caches + .keys() + .then((names) => { + const cacheName = names.find((name) => name.startsWith('QwikBuild')); + if (cacheName) { + caches.delete(cacheName).catch(console.error); + } + }) + .catch(console.error); + } } else { // eslint-disable-next-line no-console console.log('Service worker not supported in this browser.'); From ce1f74ed2e667cc7e2a4092bec196a6b6cf6bd71 Mon Sep 17 00:00:00 2001 From: maiieul Date: Wed, 27 Aug 2025 19:59:04 +0200 Subject: [PATCH 6/6] refactor: only check if service-worker.ts is removed to unregister --- .changeset/thin-horses-bake.md | 4 ++- .../generate-service-worker.ts | 10 +++---- .../qwik-city/src/buildtime/vite/plugin.ts | 27 ++----------------- .../src/runtime/src/sw-component.tsx | 6 +++++ 4 files changed, 14 insertions(+), 33 deletions(-) diff --git a/.changeset/thin-horses-bake.md b/.changeset/thin-horses-bake.md index b542bb3f20a..bd9d2bcde02 100644 --- a/.changeset/thin-horses-bake.md +++ b/.changeset/thin-horses-bake.md @@ -2,4 +2,6 @@ '@builder.io/qwik-city': patch --- -FIX: Your service-worker.js won't be unregistered anymore if you added custom logic to it. +FIX: Your service-worker.js won't be unregistered anymore if you added custom logic to it. + +> Note: Qwik 1.14.0 and above now use `` by default. If you didn't add custom service-worker logic, you should remove your service-worker.ts file(s) for the `ServiceWorkerRegister` Component to actually unregister the service-worker.js and delete its related cache. Make sure to keep the `ServiceWorkerRegister` Component in your app (without any service-worker.ts file) as long as you want to unregister the service-worker.js for your users. diff --git a/packages/qwik-city/src/buildtime/runtime-generation/generate-service-worker.ts b/packages/qwik-city/src/buildtime/runtime-generation/generate-service-worker.ts index ac1a7a8c0f7..5f291122507 100644 --- a/packages/qwik-city/src/buildtime/runtime-generation/generate-service-worker.ts +++ b/packages/qwik-city/src/buildtime/runtime-generation/generate-service-worker.ts @@ -1,15 +1,11 @@ import type { BuildContext } from '../types'; -export function generateServiceWorkerRegister( - ctx: BuildContext, - swRegister: string, - didNotAddCustomCode: boolean -) { +export function generateServiceWorkerRegister(ctx: BuildContext, swRegister: string) { let swReg: string; let swUrl = '/service-worker.js'; - // Also unregister if the developer removed the service-worker.ts file or did not add custom code to it; since Qwik 1.14.0 and above now use modulepreload by default - if (ctx.isDevServer || didNotAddCustomCode || ctx.serviceWorkers.length === 0) { + // Also unregister if the developer removed the service-worker.ts file since Qwik 1.14.0 and above now use modulepreload by default + if (ctx.isDevServer || ctx.serviceWorkers.length === 0) { swReg = SW_UNREGISTER; } else { swReg = swRegister; diff --git a/packages/qwik-city/src/buildtime/vite/plugin.ts b/packages/qwik-city/src/buildtime/vite/plugin.ts index 44c18b7e99b..e9d0d2ae287 100644 --- a/packages/qwik-city/src/buildtime/vite/plugin.ts +++ b/packages/qwik-city/src/buildtime/vite/plugin.ts @@ -186,35 +186,12 @@ function qwikCityPlugin(userOpts?: QwikCityVitePluginOptions): any { } if (isSwRegister) { - // Detect if the developer added custom code in their service-worker.ts file - let didNotAddCustomCode = false; - const clientOutDir = qwikPlugin!.api.getClientOutDir(); - if (clientOutDir) { - const basePathRelDir = api.getBasePathname().replace(/^\/|\/$/, ''); - const clientOutBaseDir = join(clientOutDir, basePathRelDir); - for (const swEntry of ctx.serviceWorkers) { - try { - const swClientDistPath = join(clientOutBaseDir, swEntry.chunkFileName); - const swCode = await fs.promises.readFile(swClientDistPath, 'utf-8'); - const swCodeTrimmed = swCode.replace(/\s+/g, ''); - if ( - swCodeTrimmed === - 'addEventListener("install",()=>self.skipWaiting());addEventListener("activate",()=>self.clients.claim());' || - swCodeTrimmed === - 'self.addEventListener("install",()=>self.skipWaiting());self.addEventListener("activate",()=>self.clients.claim());' - ) { - didNotAddCustomCode = true; - } - } catch (e) { - console.error('Error reading service worker file', e); - } - } - } // @qwik-city-sw-register - return generateServiceWorkerRegister(ctx, swRegister, didNotAddCustomCode); + return generateServiceWorkerRegister(ctx, swRegister); } } } + return null; }, diff --git a/packages/qwik-city/src/runtime/src/sw-component.tsx b/packages/qwik-city/src/runtime/src/sw-component.tsx index 2aa9bbd8c0c..8b020818708 100644 --- a/packages/qwik-city/src/runtime/src/sw-component.tsx +++ b/packages/qwik-city/src/runtime/src/sw-component.tsx @@ -5,6 +5,12 @@ import type { JSXOutput } from '@builder.io/qwik'; * JS extensions are allowed) will be picked up, bundled into a separate file, and registered as a * service worker. * + * Qwik 1.14.0 and above now use `` by default. If you didn't add custom + * service-worker logic, you should remove your service-worker.ts file(s) for the + * `ServiceWorkerRegister` Component to actually unregister the service-worker.js and delete its + * related cache. Make sure to keep the `ServiceWorkerRegister` Component in your app (without any + * service-worker.ts file) as long as you want to unregister the service-worker.js for your users. + * * @public */ export const ServiceWorkerRegister = (props: { nonce?: string }): JSXOutput => (