Skip to content

Instantly share code, notes, and snippets.

@Dabolus
Last active June 26, 2020 01:39
Show Gist options
  • Star 30 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save Dabolus/314bd939959ebe68f57f1dcebe120a7e to your computer and use it in GitHub Desktop.
Save Dabolus/314bd939959ebe68f57f1dcebe120a7e to your computer and use it in GitHub Desktop.
A simple guide that explains how to deploy PWA Starter Kit to Firebase

Note: this guide explains step by step how to add Firebase to PWA Starter Kit.

If you already have some Firebase knowledge and you just want to get everything ready out of the box, you might want to checkout the Firebase branch on the PWA Starter Kit repo, that already contains all the needed files.

Deploying prpl-server to Firebase

Firebase Hosting alone is not sufficient for hosting the prpl-server build since it requires some server-side processing of the user agent string. Instead, you will have to use Firebase Functions for that.

  1. Sign up for a Firebase account

  2. Head over the Firebase Console and create your project. Make note of the project ID associated with your app

  3. Install Firebase Command Line Tools:

    $ npm i -g firebase-tools
  4. Log into your Firebase account from the CLI by running:

    $ firebase login
  5. Set the default Firebase project for your PWA. You can do that by either running this command:

    $ firebase use --add <project-id>

    Or by creating a file named .firebaserc in the root of your application, containing:

    {
      "projects": {
        "default": "<project-id>"
      }
    }
  6. Create a file named firebase.json in the root of you PWA and paste this JSON in it:

    {
      "hosting": {
        "public": "build",
        "ignore": [
          "firebase.json",
          "**/.*"
        ],
        "rewrites": [
          {
            "source": "**",
            "function": "app"
          }
        ],
        "headers": [
          {
            "source" : "**/service-worker.js",
            "headers" : [
              {
                "key" : "Service-Worker-Allowed",
                "value" : "/"
              },
              {
                "key" : "Cache-Control",
                "value" : "no-cache"
              }
            ]
          }
        ]
      }
    }

    The public field tells Firebase that the build folder should be statically served through its CDN. The rewrites field tells Firebase to send each request to the app function - we will create it later. The headers field tells Firebase to add the Service-Worker-Allowed header when the user requests a service-worker.js file

  7. Create a functions folder. This folder will contain the code executed by Firebase Functions. You will need to create two files in it:

    1. package.json
      {
        "name": "functions",
        "description": "Cloud Functions for Firebase",
        "scripts": {
          "serve": "firebase serve --only functions",
          "shell": "firebase functions:shell",
          "start": "npm run shell",
          "deploy": "firebase deploy --only functions",
          "logs": "firebase functions:log"
        },
        "dependencies": {
          "express": "^4.16.4",
          "firebase-functions": "^2.1.0",
          "prpl-server": "^1.3.0",
          "rendertron-middleware": "^0.1.3"
        },
        "private": true
      }
    2. index.js
      const functions = require('firebase-functions');
      const prpl = require('prpl-server');
      const express = require('express');
      const rendertron = require('rendertron-middleware');
      
      const app = express();
      
      const rendertronMiddleware = rendertron.makeMiddleware({
        proxyUrl: 'https://render-tron.appspot.com/render',
        injectShadyDom: true,
      });
      
      app.use((req, res, next) => {
        req.headers['host'] = '<YOUR HOST URL HERE>';
        return rendertronMiddleware(req, res, next);
      });
      
      app.get('/*', prpl.makeHandler('./build', require('./build/polymer.json')));
      
      exports.app = functions.https.onRequest(app);
      This is the main file for our Firebase Functions. First, we create an Express app. Then, we add a rendertron middleware, that will server side render our PWA for crawler bots, and we give to PRPL Server the responsability to answer to any other GET request. Last but not least, we tell Firebase to make our Express app handle each request to the app function.
  8. Install the npm dependencies inside the functions directory:

    $ cd functions && npm i
  9. Now that the Firebase Functions part is ready, we have to setup our build to make it work correctly with it. Since Firebase Functions cannot require files that are not inside the functions directory, we need to build the PWA and make Gulp move the files needed by PRPL Server to the functions directory, while leaving the static assets in the build directory. To do that, we are going to add a new task to gulpfile.js:

    /**
     * Builds the Firebase-ready version of the PWA, moving the necessary
     * files to the functions folder to be used by PRPL Server
     */
    gulp.task('firebase', () => {
      // These are the files needed by PRPL Server, that are going to be moved to the functions folder
      const filesToMove = [ 'build/polymer.json', 'build/**/index.html', 'build/**/push-manifest.json' ];
      // Delete the build folder inside the functions folder
      return del('functions/build')
        .then(() =>
          // Copy the files needed by PRPL Server
          new Promise((resolve) =>
            gulp
              .src(filesToMove, { base: '.' })
              .pipe(gulp.dest('functions'))
              .on('end', resolve)))
        // Delete them from the original build
        .then(() => del(filesToMove));
    });
  10. Optional: If you also need to use Firebase Auth, you have to make sure to tell the Service Worker to ignore its reserved namespace. Add the following code to your sw-precache-config.js to tell your Service Worker to use index.html as a fallback for everything but Firebase reserved namespace:

    module.exports = {
      ...
      navigateFallback: '/index.html',
      navigateFallbackWhitelist: [ /^\/[^\_]+\/?/ ]
    };
  11. Add a script named build:firebase in your project root's package.json:

    "build:firebase": "polymer build --auto-base-path && gulp firebase"
    
  12. Now that you have everything set up, just run the build:firebase script to build the PWA for Firebase:

    $ npm run build:firebase
  13. Finally, deploy your PWA to Firebase:

    $ firebase deploy --only functions,hosting
@itsDineth
Copy link

itsDineth commented Jun 16, 2018

Hi, the URL passed to Rendertron seems to be of the function's URL though (*. cloudfunctions.net)—as opposed to the request URL (hosting)

@lucyllewy
Copy link

lucyllewy commented Jun 18, 2018

the service worker needs to be uncached. By default firebase hosting adds a cache-control: max-age=3600 header, so you need to add an extra element in the headers object for **/service-worker.js to ensure that it isn't cached:

"headers": [{
    "source": "**/service-worker.js",
    "headers": [
        {
            "key": "Service-Worker-Allowed",
            "value": "/"
        },{
            "key": "Cache-Control",
            "value": "no-cache"
        }
    ]
}]

@frankiefu
Copy link

prpl-server looks for push-manifest.json in the build folder. So you will need to copy push-manifest.json to functions/build/**/ or it won't generate the link headers for push. Something like this:

/**
 * Builds the Firebase-ready version of the PWA, moving the necessary
 * files to the functions folder to be used by PRPL Server
 */
gulp.task('firebase', () => {
  // These are the files needed by PRPL Server, that are going to be moved to the functions folder
  const filesToMove = [ 'build/polymer.json', 'build/**/index.html', 'build/**/push-manifest.json' ];
  // Delete the build folder inside the functions folder
  return del('functions/build')
    .then(() =>
      // Copy the files needed by PRPL Server
      new Promise((resolve) =>
        gulp
          .src(filesToMove, { base: '.' })
          .pipe(gulp.dest('functions'))
          .on('end', resolve)))
    // Delete them from the original build
    .then(() => del(filesToMove));
});

@keanulee
Copy link

rendertron.makeMiddleware() doesn't work because the req argument that Firebase Functions gives is based on a different URL (e.g. https://us-central1-pwa-starter-kit.cloudfunctions.net/app/). This URL cannot be rendered by Rendertron/browsers because static assets are not available at this host. I modified the handler in index.js to make Rendertron work (demo https://pwa-starter-kit-4225c.firebaseapp.com/):

const rendertronMiddleware = rendertron.makeMiddleware({
  proxyUrl: 'https://render-tron.appspot.com/render',
  injectShadyDom: true,
});

app.use((req, res, next) => {
  req.headers['host'] = 'pwa-starter-kit-4225c.firebaseapp.com';
  return rendertronMiddleware(req, res, next);
});

@PiusWilli
Copy link

Unfortunately i stuck on implementing firebase Auth on my test pwa.
The auth Popup is blank and i get an Error in the console
{code: "auth/network-request-failed", message: "A network error (such as timeout, interrupted connection or unreachable host) has occurred."}

login(){
    var provider = new firebase.auth.GoogleAuthProvider();
    var auth = firebase.auth();
    auth.signInWithPopup(provider)
        .then((result) => console.log('Signin result', result))
        .catch((error) => console.error('Sigin error', error));

I think it has something to do with the Reserved URL __/auth/.. which is disturbed by the service worker
Reserved URLs
Investigation the Network traffic, i see the following URL is responded by the service worker instead of firebase auth.
https://mytestproject.firebaseapp.com/__/auth/iframe?apiKey=AIzaSyDcQ0xyz......................

Does someone know how to ignor "/__/auth/*" urls by the service worker. Or am i on the wrong track, and the service worker has nothing to do with the issue?

@Dabolus
Copy link
Author

Dabolus commented Jul 11, 2018

Thanks to everyone, I added your solutions to the gist.
@keanulee I also added a note that points to your firebase branch on the Starter Kit.
@PiusWilli take a look at the new optional step 10, it should solve your issue.

@PiusWilli
Copy link

@Dabolus thx a lot!

@HeratPatel
Copy link

@PiusWilli
Thanks for the wonderful gist,
I have a question.
Do we need 'firebase-admin' dependency in functions folder ?

@Dabolus
Copy link
Author

Dabolus commented Dec 7, 2018

@HeratPatel yeah actually firebase-admin is there when you generate your functions project with firebase init, but it shouldn't be needed. I think it's safe to remove it.

EDIT:
I was wrong, firebase-admin is actually needed as a dependency, otherwise your function won't work.

@metalaboratorio
Copy link

After optional alteration in 10 (sw-precache-config.js lines), the chrome lighthouse audit (progressive web app) drop 100 to 69.
Why this happens?

@testforautism
Copy link

I am getting the foll error on running npm run build:firebase. Plz help
@Dabolus

npm ERR! file /home/nitin/fb/package.json
npm ERR! code EJSONPARSE
npm ERR! JSON.parse Failed to parse json
npm ERR! JSON.parse Unexpected string in JSON at position 1165 while parsing '{
npm ERR! JSON.parse "name": "@polymer/tfa",
npm ERR! JSON.parse "version":'
npm ERR! JSON.parse Failed to parse package.json data.
npm ERR! JSON.parse package.json must be actual JSON, not just JavaScript.

npm ERR! A complete log of this run can be found in:
npm ERR! /home/nitin/.npm/_logs/2019-04-04T16_39_00_707Z-debug.log

@rickymarcon
Copy link

rickymarcon commented Apr 8, 2019

@Dabolus You can refactor the following:

req.headers['host'] = '<YOUR HOST URL HERE>';

to

req.headers['host'] = `${process.env.GCLOUD_PROJECT}.firebaseapp.com`;

I also found out you'll run into CORS issues if you change use any other firebase region that is not us-central1 for the Cloud Function.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment