loading...
Cover image for Replace Environment Variables In Your Index.html

Replace Environment Variables In Your Index.html

daviddalbusco profile image David Dal Busco Originally published at Medium ・4 min read

Yesterday evening I began that crazy challenge to share a blog post each and every day until the quarantine is over here in Switzerland the 19th April 2020, 33 days left until hopefully better days.

In this second series’ article I would like to share with you another trick we have developed in our project DeckDeckGo.

Even if we are open source and even share the credentials of our test environment directly in our GitHub repo, we are keeping some, really few, production tokens hidden. Mostly because these are linked with our private credit cards 😅. That’s why, we have to replace environment variables at build time.

We have developed our frontend eco-system with the amazing compiler and toolchain StencilJS and I’ve already shared our solution to use variables in our code in two distinct posts (see here and there). But, what I did not share so far is, how we replace environment variables in our index.html without any plugins 😃.

Lifecycle NPM Scripts

We want to replace variables after the build as completed. To hook on a corresponding lifecycle we are using npm-scripts most precisely we are using postbuild . In our project, we create a vanilla Javascript file, for example config.index.js , and we reference it in the package.json file.

"scripts": {
  "postbuild": "./config.index.js",
}

Add Variable In Index.html

Before implementing the script to update the variable per se, let’s first add a variable in our index.html . For example, let’s add a variable <@API_URL@> for the url of the API in our CSP rule.

Of course, out of the box, this content security policy will not be compliant as <@API_URL@> isn’t a valid url. Fortunately, in such case, the browser simply ignore the rule, which can be seen as convenient, because we can therefore work locally without any problems and without having to replace the value 😄.

<meta
  http-equiv="Content-Security-Policy"
  content="default-src 'self';
  connect-src 'self' <@API_URLS@>"
/>

Update Script

Configuration is in place, variable has been added, we just have now to implement the script. Basically, what it does, it finds all html pages (we use pre-rendering, therefore our bundle contains more than a single index.html ) and for each of these, read the content, replace the variable we have defined with a regex (not the clever one, I’m agree) and write back the results.

#!/usr/bin/env node

const fs = require('fs');
const path = require('path');

function updateCSP(filename) {
  fs.readFile(`${filename}`, 'utf8', function(err, data) {
    if (err) {
      return console.log(err);
    }

    const result =
          data.replace(/<@API_URLS@>/g, `https://myapi.com`);

    fs.writeFile(`${filename}`, result, 'utf8', function(err) {
      if (err) return console.log(err);
    });
  });
}

function findHTMLFiles(dir, files) {
  fs.readdirSync(dir).forEach((file) => {
    const fullPath = path.join(dir, file);
    if (fs.lstatSync(fullPath).isDirectory()) {
      findHTMLFiles(fullPath, files);
    } else if (path.extname(fullPath) === '.html') {
      files.push(fullPath);
    }
  });
}

let htmlFiles = [];
findHTMLFiles('./www/', htmlFiles);

for (const file of htmlFiles) {
  updateCSP(`./${file}`);
}

Voilà, we are updating automatically at build time our environment variables in our application index.html 🎉

Generate SHA-256 For Your CSP

The above solution is cool but we actually had to go deeper. Each time we build our app, a script is going to be injected in our index.html in order to load the service worker. As we want to apply strict CSP rules, this script is going to be invalidated until we provide a SHA-256 exception for its representation. Of course, we weren’t looking forward to calculate it on each build and we have automated that task too. To do so, let’s first add a new variable in your index.html .

<meta
  http-equiv="Content-Security-Policy"
  content="default-src 'self';
  connect-src 'self' <@API_URLS@>"
  script-src 'self' <@SW_LOADER@>
/>

Once done, we now enhance the update script with a new function which takes care of finding the loading script (once again, not the cutest detection pattern, I’m agree), once found, generates its SHA-256 value and inject it as a new variable.

#!/usr/bin/env node

const fs = require('fs');
const path = require('path');

const crypto = require('crypto');

function updateCSP(filename) {
  fs.readFile(`${filename}`, 'utf8', function(err, data) {
    if (err) {
      return console.log(err);
    }

    let result = data.replace(/<@API_URLS@>/g, `https://myapi.com`);

    const swHash = findSWHash(data);
    if (swHash) {
      result = result.replace(/<@SW_LOADER@>/g, swHash);
    }

    fs.writeFile(`${filename}`, result, 'utf8', function(err) {
      if (err) return console.log(err);
    });
  });
}

function findSWHash(data) {
  const sw = /(<.?script data-build.*?>)([\s\S]*?)(<\/script>)/gm;

  let m;
  while ((m = sw.exec(data))) {
    if (m && m.length >= 3 && m[2].indexOf('serviceWorker') > -1) {
      return `'sha256-${crypto
        .createHash('sha256')
        .update(m[2])
        .digest('base64')}'`;
    }
  }

  return undefined;
}

function findHTMLFiles(dir, files) {
  fs.readdirSync(dir).forEach((file) => {
    const fullPath = path.join(dir, file);
    if (fs.lstatSync(fullPath).isDirectory()) {
      findHTMLFiles(fullPath, files);
    } else if (path.extname(fullPath) === '.html') {
      files.push(fullPath);
    }
  });
}

let htmlFiles = [];
findHTMLFiles('./www/', htmlFiles);

for (const file of htmlFiles) {
  updateCSP(`./${file}`);
}

That’s it, isn’t this handy?

Summary

As I said above, the regex and selector I used above aren’t the most beautiful one, but you know what, I’m not against improvements. If you are into it, don’t hesitate to send me a Pull Request 😁.

Stay home, stay safe!

David

Cover photo by Joshua Earle on Unsplash

Posted on by:

daviddalbusco profile

David Dal Busco

@daviddalbusco

Creator of DeckDeckGo | Organizer of the Ionic Zürich Meetup

Discussion

pic
Editor guide
 

I would recommend using some templating engine (ie. ejs) for doing things like that - should be less error prone.

 

What do you mean? How would you solve this in a rollup build for example if you want to apply the variables after the build has completed? Happy to hear about any possible improvements :)

 

Well, ejs takes a template (string, from whatever source, ie. from a file) and data in form of the object (which you take from whatever source) and outputs a string (which you handle however you want), that you can save to a file.

You can either write very simple rollup plugin, or use existing one (i assume there is already one), or run it as node myscript.js - "Its just javascript", as they say :)

Oh now I understand, thank you for the explanation. That's an option but then your compiler should accept it respectively it's probably only possible if you can hook on a pre-build lifecycle or something, but yes, that's a good idea! Thx for the feedback.