DEV Community

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

Replace Environment Variables In Your Index.html

David Dal Busco
Creator of DeckDeckGo | Organizer of the Ionic Zürich Meetup
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",
}
Enter fullscreen mode Exit fullscreen mode

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@>"
/>
Enter fullscreen mode Exit fullscreen mode

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}`);
}
Enter fullscreen mode Exit fullscreen mode

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@>
/>
Enter fullscreen mode Exit fullscreen mode

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}`);
}
Enter fullscreen mode Exit fullscreen mode

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

Discussion (4)

Collapse
pavelloz profile image
Paweł Kowalski

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

Collapse
daviddalbusco profile image
David Dal Busco Author • Edited

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 :)

Collapse
pavelloz profile image
Paweł Kowalski

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 :)

Thread Thread
daviddalbusco profile image
David Dal Busco Author

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.