Automatic reload after deploy


#1

Hi,

how do we make sure clients refresh their browsers after deploy of a new version?

I came across this post: https://blog.nodeswat.com/automagic-reload-for-clients-after-deploy-with-angular-4-8440c9fdd96c that checks some hashvalues stored in the client code against a hash stored in a file on the server. I would like something similar in my Aurelia app.

It seems to require a post-build task to store the hash value in a file on server and inject it into client code for runtime comparison.

Has anyone implemented a self-update component for for Aurelia app?


#2

So I modified the task in the example to fit an Aurelia project:
post-build.ts:


function injectHash() {
  const path = require('path');
  const fs = require('fs');
  const util = require('util');

  // get application version from package.json
  const appVersion = require('../../package.json').version;

  // promisify core API's
  const readDir = util.promisify(fs.readdir);
  const writeFile = util.promisify(fs.writeFile);
  const readFile = util.promisify(fs.readFile);

  // our version.json will be in the dist folder
  const versionFilePath = path.join(__dirname + '/../../scripts/version.json');

  let mainHash = '';
  let mainBundleFile = '';

  // RegExp to find main.bundle.js, even if it doesn't include a hash in it's name (dev build)
  let mainBundleRegexp = /^app.bundle.([a-z0-9]*)?.js$/;

  // read the dist folder files and find the one we're looking for
  return readDir(path.join(__dirname, '../../scripts/'))
    .then(files => {
      mainBundleFile = files.find(f => mainBundleRegexp.test(f));

      if (mainBundleFile) {
        let matchHash = mainBundleFile.match(mainBundleRegexp);

        // if it has a hash in it's name, mark it down
        if (matchHash.length > 1 && !!matchHash[1]) {
          mainHash = matchHash[1];
        }
      }

      console.log(`Writing version and hash to ${versionFilePath}`);

      // write current version and hash into the version.json file
      const src = `{"version": "${appVersion}", "hash": "${mainHash}"}`;
      return writeFile(versionFilePath, src);
    }).then(() => {
      // main bundle file not found, dev build?
      if (!mainBundleFile) {

        return Promise.resolve();
      }

      console.log(`Replacing hash-variable in the ${mainBundleFile} with value '${mainHash}'`);

      // replace hash placeholder in our main.js file so the code knows it's current hash
      const mainFilepath = path.join(__dirname, '../../scripts/', mainBundleFile);
      return readFile(mainFilepath, 'utf8')
        .then(mainFileData => {
          const replacedFile = mainFileData.replace('{{POST_BUILD_ENTERS_HASH_HERE}}', mainHash);
          return writeFile(mainFilepath, replacedFile);
        });
    }).catch(err => {
      console.log('Error with post build:', err);
    });
  }

export default function postBuild() {
  return injectHash();
}

Add this line in build.ts:

import postBuild from './post-build';

…and add the postBuild task in the build variable:

let build = gulp.series(
  readProjectConfiguration,
  gulp.parallel(
    transpile,
    processMarkup,
    processCSS,
    copyFiles,
    prepareFontAwesome,
  ),
  writeBundles,
  postBuild
);

That was easier than expected, hopefully the client side code will be easy, too :slight_smile:

Edit:
version-check-service.ts:

import { HttpClient } from 'aurelia-fetch-client';
import { autoinject } from 'aurelia-framework';
//https://blog.nodeswat.com/automagic-reload-for-clients-after-deploy-with-angular-4-8440c9fdd96c

@autoinject
export class VersionCheckService {
    // this will be replaced by actual hash post-build.js
    private currentHash = '{{POST_BUILD_ENTERS_HASH_HERE}}';
    constructor(private http: HttpClient) {
      
    }

    /**
     * Checks in every set frequency the version of frontend application
     * @param url
     * @param {number} frequency - in milliseconds, defaults to 30 minutes
     */
    public initVersionCheck(url, frequency = 1000 * 60 * 1) {
        setInterval(() => {
            this.checkVersion(url);
        }, frequency);
    }

    /**
     * Will do the call and check if the hash has changed or not
     * @param url
     */
    private checkVersion(url) {
      // timestamp these requests to invalidate caches
      this.http.fetch(url + '?t=' + new Date().getTime())
      .then(r => r.json())
      .then( (response: any) => 
        {
          const hash = response.hash;
          console.log('Current hash: ' + this.currentHash);
          console.log('New hash    : ', hash);
          const hashChanged = this.hasHashChanged(this.currentHash, hash);

          // If new version, do something
          if (hashChanged) {
              console.log('Change!');
              location.reload(true);
          } else {
            console.log('No change');
          }
          // store the new hash so we wouldn't trigger versionChange again
          // only necessary in case you did not force refresh
          //this.currentHash = hash;
          },
          (err) => {
              console.error(err, 'Could not get version');
          }
      );
    }

    /**
     * Checks if hash has changed.
     * This file has the JS hash, if it is a different one than in the version.json
     * we are dealing with version change
     * @param currentHash
     * @param newHash
     * @returns {boolean}
     */
    private hasHashChanged(currentHash, newHash) {
      if (!currentHash || currentHash === '{{POST_BUILD_ENTERS_HASH_HERE}}') {
          return false;
      }

      return currentHash !== newHash;
    }
}