3.3 Understanding the Electron Files

The two main electron files are package.json and main.js. You shouldn’t need to change these if you are editting the app but they are useful to understand and may need to be editted at a later date. The package-json.lock file is automatically created when you call npm install and keeps a record of the required javascript libraries and versions.

package.json

Firstly, the package.json file. The current contents as of 27-01-20 is displayed below.

{
  "name": "electron-quick-start",
  "version": "1.0.0",
  "description": "A minimal Electron application",
  "main": "main.js",
  "scripts": {
    "start": "electron .",
    "package-mac": "electron-packager . --overwrite --platform=darwin --arch=x64 --out=ElectronShinyAppMac",
    "package-linux": "electron-packager . --overwrite --platform=linux --arch=x64 --icon=assets/icons/png/1024x1024.png --prune=true --out=release-builds"
  },
  "repository": "https://github.com/electron/electron-quick-start",
  "keywords": [
    "Electron",
    "quick",
    "start",
    "tutorial",
    "demo"
  ],
  "author": "GitHub",
  "devDependencies": {
    "electron": "7.1.9"
  }
}

This file displays details about the app, such as the name and version (I don’t really bother updating this because it is noted in the release notes). It is all relatively intuitive. The main lines to focus on are:

"main": "main.js",

This points to the file which is ran when the app is started.

"package-mac": "electron-packager . --overwrite --platform=darwin --arch=x64 --out=ElectronShinyAppMac",
"package-linux": "electron-packager . --overwrite --platform=linux --arch=x64 --icon=assets/icons/png/1024x1024.png

These two lines are alias’ for compiling the code into an executable. It is what actually gets ran when you type npm run package-mac or package-linux. Packaging the app for linux isn’t something I’ve tried yet but it should be possible after a couple of changes to main.js. Similarly this could be done for windows too if you add the line:

"package-win": "electron-packager . --overwrite --platform=win32 --arch=ia32 --icon=cc.ico --out=ElectronShinyAppWindows --version-string.CompanyName=ColumbusCollaboratory --version-string.FileDescription=CE --version-string.ProductName=\"Shiny Electron App\"",

I’ve currently removed this line from the app because no-one seems to use windows in the University. I have kept most of the information in this file as it originally was. It can be changed if you like.

main.js

This is the file which is called when the app is run. I have printed the most important sections below as a reference for when I describe them later.

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

const url = require('url')
const port = "9194"
const child = require('child_process');
const MACOS = "darwin"

var killStr = ""
var appPath = path.join(app.getAppPath(), "R_code", "ui.R" )
var execPath = "RScript"

if(process.platform == MACOS){

  var macAbsolutePath = path.join(app.getAppPath(), "R-Portable-Mac")
  var env_path = macAbsolutePath+((process.env.PATH)?":"+process.env.PATH:"");
  var env_libs_site = macAbsolutePath+"/library"+((process.env.R_LIBS_SITE)?":"+process.env.R_LIBS_SITE:"");
  process.env.PATH = env_path
  process.env.R_LIBS_SITE = env_libs_site
  process.env.NODE_R_HOME = macAbsolutePath

  execPath = path.join(app.getAppPath(), "R-Portable-Mac", "bin", "R" )
} else {
  throw new Error("This only works on mac, error!")
}

const childProcess = child.spawn(execPath, ["-e", "shiny::runApp(file.path('"+appPath+"'), port="+port+")"])
childProcess.stdout.on('data', (data) => {
  console.log(`stdout:${data}`)
})
childProcess.stderr.on('data', (data) => {
  console.log(`stderr:${data}`)
})

function createWindow () {

    let loading = new BrowserWindow({show: false, frame: false})
    loading.loadURL("data:text/html;charset=utf-8;base64,PGh0bWw+DQo8c3R5bGU+DQpib2R5ew0KICBwYWRkaW5nOiAxZW07DQogIGNvbG9yOiAjNzc3Ow0KICB0ZXh0LWFsaWduOiBjZW50ZXI7DQogIGZvbnQtZmFtaWx5OiAiR2lsbCBzYW5zIiwgc2Fucy1zZXJpZjsNCiAgd2lkdGg6IDgwJTsNCiAgbWFyZ2luOiAwIGF1dG87DQp9DQpoMXsNCiAgbWFyZ2luOiAxZW0gMDsNCiAgYm9yZGVyLWJvdHRvbTogMXB4IGRhc2hlZDsNCiAgcGFkZGluZy1ib3R0b206IDFlbTsNCiAgZm9udC13ZWlnaHQ6IGxpZ2h0ZXI7DQp9DQpwew0KICBmb250LXN0eWxlOiBpdGFsaWM7DQp9DQoubG9hZGVyew0KICBtYXJnaW46IDAgMCAyZW07DQogIGhlaWdodDogMTAwcHg7DQogIHdpZHRoOiAyMCU7DQogIHRleHQtYWxpZ246IGNlbnRlcjsNCiAgcGFkZGluZzogMWVtOw0KICBtYXJnaW46IDAgYXV0byAxZW07DQogIGRpc3BsYXk6IGlubGluZS1ibG9jazsNCiAgdmVydGljYWwtYWxpZ246IHRvcDsNCn0NCg0KLyoNCiAgU2V0IHRoZSBjb2xvciBvZiB0aGUgaWNvbg0KKi8NCnN2ZyBwYXRoLA0Kc3ZnIHJlY3R7DQogIGZpbGw6ICNGRjY3MDA7DQp9DQo8L3N0eWxlPg0KPGJvZHk+PCEtLSAzICAtLT4NCjxkaXYgY2xhc3M9ImxvYWRlciBsb2FkZXItLXN0eWxlMyIgdGl0bGU9IjIiPg0KICA8c3ZnIHZlcnNpb249IjEuMSIgaWQ9ImxvYWRlci0xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCINCiAgICAgd2lkdGg9IjgwcHgiIGhlaWdodD0iODBweCIgdmlld0JveD0iMCAwIDUwIDUwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MCA1MDsiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KICA8cGF0aCBmaWxsPSIjMDAwIiBkPSJNNDMuOTM1LDI1LjE0NWMwLTEwLjMxOC04LjM2NC0xOC42ODMtMTguNjgzLTE4LjY4M2MtMTAuMzE4LDAtMTguNjgzLDguMzY1LTE4LjY4MywxOC42ODNoNC4wNjhjMC04LjA3MSw2LjU0My0xNC42MTUsMTQuNjE1LTE0LjYxNWM4LjA3MiwwLDE0LjYxNSw2LjU0MywxNC42MTUsMTQuNjE1SDQzLjkzNXoiPg0KICAgIDxhbmltYXRlVHJhbnNmb3JtIGF0dHJpYnV0ZVR5cGU9InhtbCINCiAgICAgIGF0dHJpYnV0ZU5hbWU9InRyYW5zZm9ybSINCiAgICAgIHR5cGU9InJvdGF0ZSINCiAgICAgIGZyb209IjAgMjUgMjUiDQogICAgICB0bz0iMzYwIDI1IDI1Ig0KICAgICAgZHVyPSIwLjZzIg0KICAgICAgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiLz4NCiAgICA8L3BhdGg+DQogIDwvc3ZnPg0KPC9kaXY+DQo8L2JvZHk+DQo8L2h0bWw+");

    loading.once('show', () => {

      mainWindow = new BrowserWindow({webPreferences:{nodeIntegration:false}, show:false, width: 800, height: 600, title:""})

      mainWindow.webContents.once('dom-ready', () => {
        setTimeout( () => {
          mainWindow.show()
          mainWindow.reload()
          loading.hide()
          loading.close()

        }, 7000)

      })

      // long loading html
      mainWindow.loadURL('http://127.0.0.1:'+port)

      // Emitted when the window is closed.
      mainWindow.on('closed', function () {
        cleanUpApplication()
      })
    })
    loading.show()
}

There are some basic code features you should know. require in javascript is equivalent to loading a library in other languaes. Console.log is the same as printing to terminal and path.join combines multiple strings into a path. Most of this can be figured out from reading the code and few comments.

The primary things this code does is:

  • Sets the path to the ui.R file:
var appPath = path.join(app.getAppPath(), "R_code", "ui.R" )
  • Sets the path to the R compiler:
execPath = path.join(app.getAppPath(), "R-Portable-Mac", "bin", "R" )
  • Sets environmental variables:
var macAbsolutePath = path.join(app.getAppPath(), "R-Portable-Mac")
var env_path = macAbsolutePath+((process.env.PATH)?":"+process.env.PATH:"");
var env_libs_site = macAbsolutePath+"/library"+((process.env.R_LIBS_SITE)?":"+process.env.R_LIBS_SITE:"");
process.env.PATH = env_path
process.env.R_LIBS_SITE = env_libs_site
process.env.NODE_R_HOME = macAbsolutePath
  • Runs the shiny app:
const childProcess = child.spawn(execPath, ["-e", "shiny::runApp(file.path('"+appPath+"'), port="+port+")"])
  • Gets the loading graphic:
loading.loadURL("data:text/html;charset=utf-8;base64,PGh0bWw+D...");
  • Sets the timeout limit:
mainWindow.webContents.once('dom-ready', () => {
  setTimeout( () => {
    mainWindow.show()
    mainWindow.reload()
    loading.hide()
    loading.close()
  }, 7000)
  • Sets the address that the app is run:
const port = "9194"
mainWindow.loadURL('http://127.0.0.1:'+port)

You may want to alter the timeout limit (currently 7000) if the app is failing to load within the required time. I have also found that occasionally Catalina blocks ports for some unknown reason. You may want to kill the process manually from the terminal and change the port number (currently 9194) before running again. One final issue I previously had was that the version of electron in the npm_modules folder was outdated and throwing an error, causing the app to constantly crash. To fix this I crated a new shny app and transferred the electron module from this across, and then alterred the package.json file to match the newer version.