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

One Trick A Day (35 Part Series)

1) How To Call The Service Worker From The Web App Context 2) Replace Environment Variables In Your Index.html 3 ... 33 3) Inject JavaScript Or CSS At Runtime And On Demand 4) Sometimes You Just Need A Dumb Library 5) Internationalization with Gatsby 6) How To Declare And Use Ionic Modals With Stencil 7) Get App Name And Version In Angular 8) Deploy Apps And Functions To Firebase From A Mono Repo With GitHub Actions 9) Starting In A New Company? Think Npmrc And Git Name 10) Test Angular Pipes With Services 11) Gatsby Tricks: Viewport, CSS Modules Transition And i18n Tricks 12) Takeover The Cordova Facebook Plugin Maintenance 13) Protect Your HTTP Firebase Cloud Functions 14) Create A Menu For Your Gatsby Website Without Libs 15) Create A Modal For Your Angular App Without Libs 16) Add A Slider To You Angular App 17) Test Angular Components and Services With HTTP Mocks 18) Merge Two Objects And Array To Object In JavaScript 19) JSX For Angular Developers 20) More JSX For Angular Developers 21) Create Your Own NPM Cli 22) Third Party Service Providers. Be transparent to each other! 23) React And Web Workers 24) Angular Testing: Mock Private Functions 25) React, Web Workers and IndexedDB 26) React, Web Workers, IndexedDB and ExcelJS 27) GitHub Actions: Hide And Set Angular Environment Variables 28) JavaScript Useful Functions 29) Deeplinking in Ionic Apps With Branch.io 30) Follow-up: Web Push Notifications And PWA In 2020 31) Angular And Web Workers 32) Git Commands I Always Forget 33) An Open Source Medium Like WYSIWYG Editor 34) Currency Picker And Formatter With Ionic React 35) Develop A Konami Code For Any Apps With Stencil

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

One Trick A Day (35 Part Series)

1) How To Call The Service Worker From The Web App Context 2) Replace Environment Variables In Your Index.html 3 ... 33 3) Inject JavaScript Or CSS At Runtime And On Demand 4) Sometimes You Just Need A Dumb Library 5) Internationalization with Gatsby 6) How To Declare And Use Ionic Modals With Stencil 7) Get App Name And Version In Angular 8) Deploy Apps And Functions To Firebase From A Mono Repo With GitHub Actions 9) Starting In A New Company? Think Npmrc And Git Name 10) Test Angular Pipes With Services 11) Gatsby Tricks: Viewport, CSS Modules Transition And i18n Tricks 12) Takeover The Cordova Facebook Plugin Maintenance 13) Protect Your HTTP Firebase Cloud Functions 14) Create A Menu For Your Gatsby Website Without Libs 15) Create A Modal For Your Angular App Without Libs 16) Add A Slider To You Angular App 17) Test Angular Components and Services With HTTP Mocks 18) Merge Two Objects And Array To Object In JavaScript 19) JSX For Angular Developers 20) More JSX For Angular Developers 21) Create Your Own NPM Cli 22) Third Party Service Providers. Be transparent to each other! 23) React And Web Workers 24) Angular Testing: Mock Private Functions 25) React, Web Workers and IndexedDB 26) React, Web Workers, IndexedDB and ExcelJS 27) GitHub Actions: Hide And Set Angular Environment Variables 28) JavaScript Useful Functions 29) Deeplinking in Ionic Apps With Branch.io 30) Follow-up: Web Push Notifications And PWA In 2020 31) Angular And Web Workers 32) Git Commands I Always Forget 33) An Open Source Medium Like WYSIWYG Editor 34) Currency Picker And Formatter With Ionic React 35) Develop A Konami Code For Any Apps With Stencil

Posted on by:

daviddalbusco profile

David Dal Busco

@daviddalbusco

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

Discussion

markdown 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.