Mike Tolland

Working with Angular CLI and Electron Part 2

In the last post we worked on creating the base angular 2 app with angular 2 material and material icons in an offline mode. Looking at our list of objectives again here is what we have to do.

So we are half way through our objectives now lets tackle the next one and this is versioning of the software. Basically what I am looking to do here is to properly version the software based on git tag and where you are in a commit. In the past I mentioned a tool call GitVersion this is a C# based tool that calculates the version of the version of your software based on branch and commit. My team uses it extensively to version our software on the C# side of thing and I have been looking for a good solution on the javascript side. The GitVersion tool is very similar to a git command git describe. Running the git describe command will analyze your repo and produce a string like this

0.1.0-3-g7e9953a

If you split the string on dashes you would end up with three strings. The first 0.1.0 is the last tag in your history from the current commit checked out. The next string is 3 which represents the number of commits since that tag and the final string g7e9953a is the hash for your current commit. Its important to not that it does not by default count lightweight tags and if you need that you will have to use the this command git describe --tags. Add the flag tags will allow you to check even lightweight tags. Now this command is perfect for my purposes so I am going to use it in a script that I want to run to update the package.json. I am also going to use the nice versiony npm package to update the package.json file to my version. I am also going to use the npm package hasbin to determine if git exists on path. It should always exist if you are checking out/cloning a repo but you never know what sort of environments you are dealing with.

Here is my node script

const { spawn } = require('child_process');
var versiony = require('versiony');
var hasbin = require('hasbin');

// Target working directory

var gitExists = hasbin.sync('git');

if(gitExists){

  var describeProc = spawn('git', ['describe']);

  describeProc.stdout.on('data', (data)=>{


    var versionArray = (data+'').split('-');
    var currentVersion;
    if(versionArray.length > 1){
      var pad = "0000"
      var versionNum = pad.substring(0, pad.length - versionArray[1].length) + versionArray[1];
      currentVersion = versionArray[0]+'-dev'+versionNum;
    }else{
      currentVersion = versionArray[0];
    }
    console.log(`version: ${currentVersion}`);

    versiony
      .version(currentVersion)
      .to(__dirname+'/../package.json')


  });
}else{
  console.error("No GIT Found!");
}

Basically how this script works is that first it checks if git exists. Second it runs the git describe command and pipes the standard output to a function. The function then splits the string into an array on a ‘-‘. If the array is bigger than 1 that means we are on a commit that is not tagged itself and is a development commit. Thus we create a version string from the first part of the array and the second part of the labeled as a dev number. In our team we use semantic versioning and in particular a format of {version}-dev. The commits since last version is always 4 digits so we pad the number with 0’s if its less than four digits, 3 becomes 0003. In javascript we can do this easily using a little trick I found on stackoverflow here. Thus we get our version. Its not perfect but thats good for now. With this script I save it as updatePkg.js and add it to my configs folder. I also update my package.json file with a new script.

 "update:version": "node ./configs/updatePkg.js"

Now I tag my repo again as I finished the last task before jumping into electron. You can see the repo at this point here. Next I need to integrate electron. The first step of course is to install as devDependencies both electron and electron-packager.

After doing that I add my electron entry script to my source folder it should look something like this

const {app, BrowserWindow} = require('electron');
const path = require('path');
const url = require('url');

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win

function createWindow () {
  // Create the browser window.
  win = new BrowserWindow({width: 800, height: 600});

  // and load the index.html of the app.

  win.loadURL(url.format({
    pathname: path.join(__dirname, 'index.html'),
    protocol: 'file:',
    slashes: true
  }));

  if (process.env.NOD_ENV=='development') {
    // Open the DevTools when in dev mode.
    win.webContents.openDevTools()
  }

  // Emitted when the window is closed.
  win.on('closed', () => {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    win = null;
  });
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);

// Quit when all windows are closed.
app.on('window-all-closed', () => {
  // On macOS it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('activate', () => {
  // On macOS it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (win === null) {
    createWindow();
  }
});

This is your most basic electron entry file. Now I must get this into my dist folder where all my angular files are building to. The one problem though is that I can’t import as part of my app.module.ts or include it as an entry point in the .angular-cli.json because this will cause it try and bundle/webpack it which I don’t want. I want it to just be copied over from the src folder to the dist folder. In normal webpack I would use the CopyWebpackPlugin but unfortunately with angular cli I don’t have access to the underlying webpack. So instead I list the entry.js as an asset like so

"root": "src",
"outDir": "dist",
"assets": [
  "assets",
  "favicon.ico",
  "entry.js"
],

This will copy it directly to my dist folder without bundling or webpacking it. Next I update my package.json so that main file is the entry.js rather than anything else like so

"version": "0.2.0-dev0001",
"license": "MIT",
"main": "dist/entry.js",
"scripts": {

After doing this simply run ng build and then electron . from the root of the project and your website should not show up in an electron window. We are very close to completion at this point. Next we want to be able to test electron while running ng serve. In order to do that I install the concurrently npm package. This package allows you to run multiple processes at the same time. Then I copy my entry.js and make a new file called entry.live.js in this file I replace the loadURL portion with the following

 win.loadURL(url.format({
    pathname: 'localhost:4200',
    protocol: 'http:',
    slashes: true
  }));

This will allow electron to pick up the localhost server that ng serve is starting. Then I modify my npm script start to the following

"start": "concurrently -k \"ng serve\" \"electron src/entry.live.js\"",

This allows me to concurrently run ng serve and an electron window using the new entry.live.js file. I also add the k flag so that if I close electron ng serve will also end. A quick run of npm start and an electron window pops up and after a few seconds my website shows up in electron. At this point I can continue to edit my files and ng serve will automatically compile and refresh the screen in electron. With this I am done with all my tasks but I still want to do one more thing and that is packaging up the electron apps for distribution. I will be using electron-packager for this. I have talked about electron-packager in the past and this time I will be using it in a node script. In my configs folder I create a javascript file called electronPkg.win.js and then type out the script like so

var packager = require('electron-packager');

var pkgOptions = {
  "dir":__dirname+"\\..\\",
  "ignore":[
    "src",
    "node_modules",
    "configs",
    ".vscode",
    "packages",
    ".editorconfig",
    ".angular-cli.json",
    ".gitignore",
    "package-lock.json",
    "tsconfig.json",
    "tslint.json"
  ],
  "out":"packages.local",
  "arch":"all",
  "name":process.env.npm_package_name+"-"+process.env.npm_package_version,
  "appVersion":process.env.npm_package_version,
  "platform":"win32",
  "overwrite":true
};

packager(pkgOptions, function(err, appPaths){
  if(!err){
    for(var i in appPaths){
      console.log(appPaths[i]);
    }
  }else{
    console.log(err);
  }

});

I set the dir to be the root of my project and I ignore all files and folders I don’t want in my electron package which should be most everything but package.json and the dist folder. I then set the out to packages.local which is a folder our team uses for distribution files. The name of the electron app will be the name defined in package.json and the version which is determined by our git describe command. The packager determines the version of electron to use based on what is installed in our devDependencies. I also set overwrite to true in order to overwrite old builds when the script runs. I then add a bunch of new scripts to my package.json and it finishes up looking something like this

{
  "name": "electron-angular2",
  "version": "0.2.0-dev0001",
  "license": "MIT",
  "main": "dist/entry.js",
  "scripts": {
    "ng": "ng",
    "start": "concurrently -k \"ng serve\" \"electron src/entry.live.js\"",
    "build": "ng build",
    "update:version": "node ./configs/updatePkg.js",
    "package:linux": "node ./configs/electronPkg.linux.js",
    "package:windows": "node ./configs/electronPkg.win.js",
    "package:all": "npm run package:linux && npm run package:windows",
    "deploy": "npm run update:version && npm run build && npm run package:all",
    "test": "node ./configs/electronPkg.linux.js"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "^4.3.2",
    "@angular/cdk": "^2.0.0-beta.8",
    "@angular/common": "^4.0.0",
    "@angular/compiler": "^4.0.0",
    "@angular/core": "^4.0.0",
    "@angular/forms": "^4.0.0",
    "@angular/http": "^4.0.0",
    "@angular/material": "^2.0.0-beta.8",
    "@angular/platform-browser": "^4.0.0",
    "@angular/platform-browser-dynamic": "^4.0.0",
    "@angular/router": "^4.0.0",
    "core-js": "^2.4.1",
    "material-design-icons": "^3.0.1",
    "rxjs": "^5.4.1",
    "zone.js": "^0.8.14"
  },
  "devDependencies": {
    "@angular/cli": "1.2.6",
    "@angular/compiler-cli": "^4.0.0",
    "@angular/language-service": "^4.0.0",
    "@types/node": "~6.0.60",
    "codelyzer": "~3.0.1",
    "concurrently": "^3.5.0",
    "electron": "^1.6.11",
    "electron-packager": "^8.7.2",
    "hasbin": "^1.2.3",
    "ts-node": "~3.0.4",
    "tslint": "~5.3.2",
    "typescript": "~2.3.3",
    "versiony": "^2.0.1"
  }
}

When I run the command npm run deploy it will update the version of my package, build the code using ng build and finally electron package for linux and windows. Thats it I now have a working setup for electron, angular 2, angular cli, and even angular 2 material libraries. The final code is tagged as version 0.3.0. From here I intend to add other features this code base that will allow me to do things like splash screens. In a future post in a couple of days I will be talk about how to make a splash screen in electron.


Share this: