Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Proper way to setup firebase-messaging service worker with VITE

I have a project built with SolidJS, typescript and Vite. I already have a service worker working using vite PWA plugin, using the generate service worker strategy. Now I want to add notifications using firebase cloud messaging (FCM) and their documentation instructs you to create a very simple file that is meant to be bundled as a service worker. This presents a challenge because Vite is not really meant to be used with several entry-points, which is somewhat required to properly include this file.

I tried several approaches, and I am not happy with any of them. All feel hacky and seem to be suboptimal. Here are the things I tried, ordered from more to less success.

Adding another instance of vite PWA plugin using the injectManifest strategy.

This is the current strategy I'm using because it has the best balance between convenience and being a working solution. There is no single line in the vite-pwa-plugin documentation that says that using several instances of the plugin is possible or encouraged, but there is not anything against it either. So what I did was to instantiate the plugin two times, one for the "legit/real" service worker that my app uses, and another with the inject manifest strategy to bundle the required firebase-messaging-sw.js service worker:

Vite config:

export default defineConfig({
plugins: [
// Abuse the plugin API to bundle the messaging service worker
    VitePWA({
      strategies: 'injectManifest',
      srcDir: 'src',
      filename: 'firebase-messaging-sw.ts',
      workbox: {
        globPatterns: [],
        globIgnores: ['*'],
      },
    }),
// Real legit service worker for my app
    VitePWA({
      registerType: 'autoUpdate',
      devOptions: { enabled: true },
      // minimum PWA
      includeAssets: ['favicon.ico', 'robots.txt', '*.svg', '*.{png,ico}'],
      workbox: { 
      // bla bla bla

Service worker file firebase-messaging-sw.js:

// Import and configure the Firebase SDK
import { initializeApp } from 'firebase/app';
import { getMessaging } from 'firebase/messaging/sw';
import firebaseConfig from './firebase-config.json';
// Line below makes typescript happy by importing the definitions required for ServiceWorkerGlobalScope
import { precacheAndRoute as _ } from 'workbox-precaching';

declare let self: ServiceWorkerGlobalScope;

const firebaseApp = initializeApp(firebaseConfig);
getMessaging(firebaseApp);
console.info('Firebase messaging service worker is set up');
// If we don't include a point to inject the manifest the plugin will fail.
// Using just a variable will not work because it is tree-shaked, we need to make it part of a side effect to prevent it from being removed
console.log(self.__WB_MANIFEST);

As you can see, this approach seems to be abusing the plugin API and has some nasty side-effects that fulfills no purpose other than preventing the bundling from failing. However, it works, doesn't require a separate build file or configuration and it's everything within a single vite config. Nasty, but convenient

Creating my own plugin to bundle the service worker separately

I tried creating a local plugin to handle the imports of the firebase-messaging-sw.js file and emitting it as a separate chunk. However, when the file is registered as a service worker I get errors because the file is bundled as if it was a chunk that is part of the application, therefore it relies on bundle features (like import) that are not available on the service worker environment. The plugin code looks something like this:

import { Plugin } from 'vite';
const print = (obj) => console.dir(obj, { depth: 8 });
export function renameFirebaseSw(): Plugin {
  const virtualId = 'virtual:firebase-messaging-sw';
  const resolvedVirtualModuleId = '\0' + virtualId;
  return {
    name: 'vite:rename-firebase-sw',
    // enforce: 'post',
    resolveId(id) {
      if (id === virtualId) return resolvedVirtualModuleId;
    },
    buildStart() {
      print('start');
    },
    load(id) {
      if (id === resolvedVirtualModuleId) {
        const ref1 = this.emitFile({
          type: 'chunk',
          fileName: 'firebase-messaging-sw.js',
          id: './src/firebase-messaging-sw.ts',
        });
        console.log({ ref1 });
        return `export default import.meta.ROLLUP_FILE_URL_${ref1}`;
      }
    },
  };
}

Then, you import the virtual module from anywhere in your app, and the plugin will intercept it and emit a separate chunk with the messaging service worker file. As I mentioned before, this does not bundle the service worker code properly and fails, not to mention that it does not work on development, only when building for production

Having a separate vite config

Lastly, you can have a separate vite config just for the purpose of bundling the service worker file. This approach seems to be the cleanest because you are just bundling the service worker code like it were a separate app (which it kinda is). The problem is that you need to make sure that the output name is the appropriate one (not having a hash, and that the app that imports/registers it uses the same name), to make sure you always run the main app build step and the service worker build step and to also run them in parallel in dev mode. To be honest, not something I want to maintain.

Is there any clean and convenient way to include the required service worker for cloud messaging that does not have any of the compromises mentioned? I did a lot of research and investigation and I didn't find anything but workarounds.

like image 331
Danielo515 Avatar asked Jan 26 '26 15:01

Danielo515


1 Answers

Thanks for the detailed write up, I almost gave up on VitePWA before I read your question. There is another method that, to me, feels just right. Maybe the libraries has evolved since you've posted. The key here is explicitly telling Firebase to use existing (custom) Service Worker and ditching the firebase-messaging-sw.js convention:

VitePA Config

For VitePWA options of note, I set injectRegister to null because I will be manually registering my own custom service worker. I've also enabled dev mode and type of module to allow es6 imports. In production, VitePWA will convert this to classic for maximum compatibility.

  // vite.config.js
  plugins: [
    VitePWA({
      strategies: 'injectManifest',
      injectRegister: null,
      registerType: 'autoUpdate',
      devOptions: {
        enabled: true,
        type: 'module',
        navigateFallback: 'index.html'
      },
      workbox: {
        sourcemap: true
      }
    })
  ]

Register Service Worker and pass to Firebase

The registration part is documented in VitePWA Development page. Another thing to note here is the serviceWorkerRegistration option of Firebase's getToken. That is where you can tell Firebase to use an existing custom Service Worker.

    import { initializeApp } from "firebase/app";
    import { getMessaging, getToken, onMessage } from "firebase/messaging";
    
    const firebaseApp = initializeApp(<your-firebase-config>);
    const messaging = getMessaging(firebaseApp);

    if ("serviceWorker" in navigator) {
      navigator.serviceWorker
        .register(    
          import.meta.env.MODE === 'production' ? '/sw.js' : '/dev-sw.js?dev-sw',
          { type: import.meta.env.MODE === 'production' ? 'classic' : 'module' }
        )
        .then((registration) => {
          getToken(messaging, {
              vapidKey: '<your-vapidkey>',
              serviceWorkerRegistration : registration 
          })
            .then((currentToken) => {
              // do something
            });
         });
    }
 

Service Worker

Do stuff with your Service Worker with some Firebase bit in there too.

// public/sw.js

import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching';
import { clientsClaim } from 'workbox-core';
import { NavigationRoute, registerRoute } from 'workbox-routing';

import { initializeApp } from "firebase/app";
import { getMessaging } from "firebase/messaging/sw";

// self.__WB_MANIFEST is default injection point
precacheAndRoute(self.__WB_MANIFEST);

// clean old assets
cleanupOutdatedCaches();

let allowlist;
if (import.meta.env.DEV) {
  allowlist = [/^\/$/];
}

// to allow work offline
registerRoute(new NavigationRoute(
  createHandlerBoundToURL('index.html'),
  { allowlist },
));

const firebaseApp = initializeApp(<your-firebase-config>);

const messaging = getMessaging(firebaseApp);

self.skipWaiting();
clientsClaim();

You can run Vite the way you normally do in dev mode and your custom service worker should be working with Firebase Cloud Messaging.

I hope this helps someone. I'm at the beginning stages myself and will update if I come up with something better or run into more issues. But so far it's working great!

like image 63
janechii Avatar answered Jan 28 '26 21:01

janechii



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!