22

Edit: This looks like a duplicate of this Unresolved Question. Do I mark this as answered, or delete?

I am using InjectManifest from workbox-webpack-plugin inside a Vue CLI 3 app. The custom service worker that I am injecting into has handling for Firebase Cloud Messaging (FCM). I need to listen for messages from different senders based on my environment (local, staging, and production).

Ideally, service-worker.js would look like this:

importScripts('https://www.gstatic.com/firebasejs/4.8.1/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/4.8.1/firebase-messaging.js');

firebase.initializeApp({
    'messagingSenderId': process.env.VUE_APP_FCM_SENDER_ID
});

However, this code doesn't seem to be touched by webpack, as the output service worker still reads process.env.VUE_APP_FCM_SENDER_ID instead of the hardcoded key.

How can I run my service worker through webpack in order to resolve environment variables?

Rajar
  • 274
  • 1
  • 7
Madbarron
  • 335
  • 1
  • 2
  • 9

3 Answers3

20

It's probably way too late for you now but for others this is how I went on about getting around this. This process still utilizes .env file for your environment related variables.
The idea is to create a new script that loads the .env file which creates a new file populated with the variables from .env file.
After the build process we simply import the newly generated file in sw.js for it to be used.

Here are the steps.
First create a file called swEnvbuild.js which will be your script that runs after webpack

//swEnvBuild.js - script that is separate from webpack
require('dotenv').config(); // make sure you have '.env' file in pwd
const fs = require('fs');

fs.writeFileSync('./dist/public/swenv.js',
`
const process = {
  env: {
    VUE_APP_FCM_SENDER_ID: conf.VUE_APP_FCM_SENDER_ID
  }
}
`);

Secondly, we import the file that was generated from swEnvBuild.js called swenv.js in our sw.js.

// sw.js
importScripts('swenv.js'); // this file should have all the vars declared
console.log(process.env.VUE_APP_FCM_SENDER_ID);

And lastly, for this to work with one command just add the following in your npm scripts (assuming that you're running either Linux/Mac).

scripts: {
  "start": "webpack && node swEnvBuild.js"
}

Hopefully, that should do the trick. I wish there was much cleaner way to do this so I'd be happy to know how other's solution too.

shriek
  • 5,605
  • 8
  • 46
  • 75
  • Thanks! Because the question is tagged vue-cli-3, I made some edits to the answer to match what I had to do to make it work for me in the vue world. – Madbarron May 30 '19 at 20:30
  • For me I updated my build script in package.json to: `"build": "vue-cli-service build && node swEnvBuild.js"` – John M Jan 29 '20 at 15:31
19

I had this same problem and the key is getting the webpack build process to output the env vars that it uses so they can be imported into the service worker. This saves you from having to duplicate your env var definitions into something else that pre-processes your service worker (which is just messy anyway because that file is in source control).

  1. create a new Webpack plugin

    // <project-root>/vue-config/DumpVueEnvVarsWebpackPlugin.js
    const path = require('path')
    const fs = require('fs')
    
    const pluginName = 'DumpVueEnvVarsWebpackPlugin'
    
    /**
     * We to configure the service-worker to cache calls to both the API and the
     * static content server but these are configurable URLs. We already use the env var
     * system that vue-cli offers so implementing something outside the build
     * process that parses the service-worker file would be messy. This lets us
     * dump the env vars as configured for the rest of the app and import them into
     * the service-worker script to use them.
     *
     * We need to do this as the service-worker script is NOT processed by webpack
     * so we can't put any placeholders in it directly.
     */
    
    module.exports = class DumpVueEnvVarsWebpackPlugin {
      constructor(opts) {
        this.filename = opts.filename || 'env-vars-dump.js'
      }
    
      apply(compiler) {
        const fileContent = Object.keys(process.env)
          .filter(k => k.startsWith('VUE_APP_'))
          .reduce((accum, currKey) => {
            const val = process.env[currKey]
            accum += `const ${currKey} = '${val}'\n`
            return accum
          }, '')
        const outputDir = compiler.options.output.path
        if (!fs.existsSync(outputDir)) {
          // TODO ideally we'd let Webpack create it for us, but not sure how to
          // make this run later in the lifecycle
          fs.mkdirSync(outputDir)
        }
        const fullOutputPath = path.join(outputDir, this.filename)
        console.debug(
          `[DumpVueEnvVarsWebpackPlugin] dumping env vars to file=${fullOutputPath}`,
        )
        fs.writeFileSync(fullOutputPath, fileContent)
      }
    }
    
  2. use the plugin in your vue-cli config (vue.config.js or vue-config/config.default.js if your config is split over a few files)

    // import our plugin (change the path to where you saved the plugin script)
    const DumpVueEnvVarsWebpackPlugin = require('./DumpVueEnvVarsWebpackPlugin.js')
    
    module.exports = {
      // other stuff...
      configureWebpack: {
        plugins: [
          // We add our plugin here
          new DumpVueEnvVarsWebpackPlugin({ filename: 'my-env-vars.js' })
        ],
      },
    }
    
  3. in our service worker script, we can now import the file that we wrote with our Webpack plugin (it'll be there after the build has happened and service workers don't run in dev mode so we should be safe)

    importScripts('./my-env-vars.js') // written by DumpVueEnvVarsWebpackPlugin
    const fcmSenderId = VUE_APP_FCM_SENDER_ID // comes from script imported above
    console.debug(`Using sender ID = ${fcmSenderId}`)
    
    // use the variable
    firebase.initializeApp({
        'messagingSenderId': fcmSenderId
    })
    

It's not perfect, but it certainly gets the job done. It's D-R-Y as you only have to define all your env vars in one spot and the whole app uses the same values. Plus, it doesn't process any files that are in source control. I don't like that the plugin runs too early in the Webpack lifecycle so we have to create the dist dir but hopefully someone else smarter than me will have a fix for that.

Tom Saleeba
  • 4,031
  • 4
  • 41
  • 36
  • 3
    Saved my arse thanks man, I used this snippet to extract my Version from Package.json to use in my service worker :) thanks – Sweet Chilly Philly Nov 08 '19 at 00:46
  • 1
    Hey man, I made this into an npm package, check it out! https://www.npmjs.com/package/vue-enverywhere – Sweet Chilly Philly Nov 12 '19 at 03:03
  • 1
    This solution works without problem, thank you ! It is incredible that the developers of vue-pwa did not think of this use case. – Alex83690 Jan 07 '20 at 14:18
  • Man, you saved my day! Thank.You.So.Much – l1b3rty Jan 27 '20 at 18:18
  • 1
    Finally, exactly the answer I was looking for! Thank you very much! – PeeJee May 29 '20 at 12:17
  • 1
    @Tunvir Rahman Tusher it should do. There's not much here that's specific to Vue. You'll have to change how the env vars are discovered (this code looks for things that start with `VUE_APP`) and figure out how to add this to React's webpack pipeline. – Tom Saleeba Dec 07 '20 at 04:59
  • This can potentially be a security risk as it will expose your env vars in the `my-env-vars.js` which will be available via URL on your website. – caweidmann Apr 21 '22 at 15:08
  • @caweidmann but that's the whole point. It's a webapp, so the user is *meant* to download the env var file to their device, just like the rest of the JS, CSS and HTML files. Things in this file aren't secrets, they're configuration. – Tom Saleeba Apr 23 '22 at 02:24
  • Thought I'd mention it because some people might not be aware of this. Usually these variables are scattered throughout the app in any case and a malicious user could get hold of them, but it would be a bit more involved. Having them exposed like this makes it much easier. And agreed, you shouldn't store secrets there! – caweidmann Apr 24 '22 at 09:41
0

I was trying to do this in Nuxt for some firebase cloud messaging, and I adapted @shriek's answer above to do what I needed.


First create a file called swEnvbuild.js in root directory, as per below:

require('dotenv').config(); // make sure you have '.env' file in pwd
const fs = require('fs');

fs.writeFileSync('./public/swenv.js',
`
const process = {
  env: {
    FB_API_KEY: '${process.env.FIREBASE_API_KEY}',
    FB_AUTHDOMAIN: '${process.env.FIREBASE_AUTHDOMAIN}',
    FB_PROJECTID: '${process.env.FIREBASE_PROJECTID}',
    FB_STORAGEBUCKET: '${process.env.FIREBASE_STORAGEBUCKET}',
    FB_MESSAGINGSENDERID: '${process.env.FIREBASE_MESSAGINGSENDERID}',
    FB_APPID: '${process.env.FIREBASE_APPID}',
    FB_MEASUREMENTID: '${process.env.FIREBASE_MEASUREMENTID}',
    FB_PUSHNOTETOKEN: '${process.env.FIREBASE_PUSHNOTETOKEN}'
  }
}
`);

Then, update your service worker file with:

importScripts('https://www.gstatic.com/firebasejs/9.0.0/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/9.0.0/firebase-messaging-compat.js');

importScripts('swenv.js'); // this file should have all the vars declared, allowing you to use process.env.ANY_KEY_YOU_DEFINED

firebase.initializeApp({
  apiKey: process.env.FB_API_KEY,
  authDomain: process.env.FB_AUTHDOMAIN,
  projectId: process.env.FB_PROJECTID,
  storageBucket: process.env.FB_STORAGEBUCKET,
  messagingSenderId: process.env.FB_MESSAGINGSENDERID,
  appId: process.env.FB_APPID,
  measurementId: process.env.FB_MEASUREMENTID
});

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('../firebase-messaging-sw.js', { type: 'module', scope: '__' })
    .then(function(registration) {
      console.log('Registration successful, scope is:', registration.scope);
    }).catch(function(err) {
      console.log('Service worker registration failed, error:', err);
    });
  }

const isSupported = firebase.messaging.isSupported();
if (isSupported) {
    const messaging = firebase.messaging();
    messaging.onBackgroundMessage(({ notification: { title, body, image } }) => {
        self.registration.showNotification(title, { body, icon: image || '/assets/icons/icon-72x72.png' });
    });
}

Finally, update your package.json to build the required file

// add 'node swEnvBuild.js' to build and start script

"scripts": {
    "build": "nuxt build && node swEnvBuild.js",
    "dev": "node swEnvBuild.js && nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "start": "node .output/server/index.
}

That did it for me