TransWikia.com

Creating an installer for Electron React JS app - reactJS component doesn't load when run after install

Stack Overflow Asked by Boardy on January 28, 2021

I am working on a new project using Electron and ReactJS. The project works fine in development mode, but I am trying to create an installer for Windows but no matter what I try and what I find on Google nothing works. I just get a blank white screen.

Below is my pacakge.json

{
  "name": "MyApp",
  "description": "My App Description",
  "version": "0.1.2",
  "private": true,
  "homepage": "./",
  "dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.5.0",
    "@testing-library/user-event": "^7.2.1",
    "@types/jest": "^26.0.14",
    "@types/node": "^14.11.2",
    "@types/react": "^16.9.50",
    "@types/react-dom": "^16.9.8",
    "bootstrap": "^4.5.2",
    "electron-is-dev": "^1.2.0",
    "electron-settings": "^4.0.2",
    "electron-squirrel-startup": "^1.0.0",
    "react": "^16.13.1",
    "react-bootstrap": "^1.3.0",
    "react-dom": "^16.13.1",
    "react-icons": "^3.11.0",
    "react-json-pretty": "^2.2.0",
    "react-scripts": "3.4.3",
    "react-tooltip": "^4.2.10",
    "typescript": "^4.0.3"
  },
  "main": "src/electron-starter.js",
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "electron-start": "set ELECTRON_START_URL=http://localhost:3000 && electron .",
    "package-win": "electron-packager . --asar --out=release-builds --platform=win32 --arch=x64 --no-prune --ignore=/e2e --overwrite",
    "create-installer-win": "node installers/windows/createInstaller.js"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {

    "electron": "^10.1.3",
    "electron-packager": "^12.0.1",
    "electron-winstaller": "^4.0.1",
    "react-router-dom": "^5.2.0",
    "react-toastify": "^6.0.8"
  }
}

My electron-start.js script is below

const {electron, Menu, app, BrowserWindow} = require('electron');
// Module to control application life.
//const app = electron.app;
// Module to create native browser window.
//const BrowserWindow = electron.BrowserWindow;


const path = require('path');
const url = require('url');

if (require('electron-squirrel-startup')) app.quit()
// if first time install on windows, do not run application, rather
// let squirrel installer do its work
const setupEvents = require('../installers/setupEvents')
if (setupEvents.handleSquirrelEvent()) {
    console.log("Squirrel event returned true");
    process.exit()
    //return;
}

console.log("Starting main program");

// 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 mainWindow;

/*const env = process.env.NODE_ENV;

let windowUrlBase = "";

if (env === "production")
{
    windowUrlBase = "/";
}
else
{
    windowUrlBase = 'http://localhost:3000';
}*/

let windowUrlBase = 'http://localhost:3000';

function returnMainWindow()
{
    const mainWindow =  new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            nodeIntegration: true,
            //preload: __dirname + '/preload.tsx'
        }
    });

    //const env = process.env.NODE_ENV;
    //console.log("Environment: " + env);
    const isDev = require('electron-is-dev');

    windowUrlBase = "";
    console.log("Not electron dev");
    console.log("dir name: " + __dirname);

    const startUrl = process.env.ELECTRON_START_URL || url.format({
        //pathname: path.join(__dirname, '/../build/index.html'),
        pathname: path.join(__dirname, '../index.html'),
        protocol: 'file:',
        slashes: true,
        webSecurity: false
    });
    mainWindow.loadURL(startUrl);
    
    return mainWindow;
}


function createWindow() {
    // Create the browser window.

    mainWindow = returnMainWindow();



    mainWindow.maximize();

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


    // Open the DevTools.
    //mainWindow.webContents.openDevTools();

    // Emitted when the window is closed.
    mainWindow.on('closed', function () {
        // 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.
        mainWindow = null
    })
    setMainMenu();
}

function setMainMenu()
{
    const template = [
        {
            label: 'File',
            submenu: [
                {
                    label: 'Exit',
                    accelerator: "ctrl+f4",
                    click() {
                        app.quit();
                    }
                }
            ]
        },
        {
            label: 'Edit',
            submenu: [
                {
                    label: 'Settings',
                    click() {
                        mainWindow.loadURL(windowUrlBase + "/settings");
                    }
                }
            ]
        },
        {
            label: 'Help',
            submenu: [
                {
                    label: 'Show Dev Console',
                    accelerator: "f11",
                    click() {
                        mainWindow.webContents.openDevTools();
                    }
                }
            ]
        }
    ];

    Menu.setApplicationMenu(Menu.buildFromTemplate(template));
}

// 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', function () {
    // On OS X 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', function () {
    // On OS X 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 (mainWindow === null) {
        createWindow()
    }
});

My create installer script is below

const createWindowsInstaller = require('electron-winstaller').createWindowsInstaller
const path = require('path')

getInstallerConfig()
    .then(createWindowsInstaller)
    .catch((error) => {
        console.error(error.message || error)
        process.exit(1)
    })

function getInstallerConfig () {
    console.log('creating windows installer')
    const rootPath = path.join('./')
    const outPath = path.join(rootPath, 'release-builds')

    return Promise.resolve({

        appDirectory: path.join(outPath, 'crash-catch-control-panel-win32-x64'),
        authors: 'Boardies IT Solutions',
        noMsi: true,
        outputDirectory: path.join(outPath, 'windows-installer'),
        exe: 'crash-catch-control-panel.exe',
        setupExe: 'crash-catch-control-panel-installer.exe'
        //setupIcon: path.join(rootPath, 'assets', 'images', 'logo.ico')
    })
}

My setupEvents.js is below

const electron = require('electron')
const app = electron.app

module.exports = {
    handleSquirrelEvent: function() {
        if (process.argv.length === 1) {
            return false;
        }

        const ChildProcess = require('child_process');
        const path = require('path');

        const appFolder = path.resolve(process.execPath, '..');
        const rootAtomFolder = path.resolve(appFolder, '..');
        const updateDotExe = path.resolve(path.join(rootAtomFolder, 'Update.exe'));
        const exeName = path.basename(process.execPath);
        const spawn = function(command, args) {
            let spawnedProcess, error;

            try {
                spawnedProcess = ChildProcess.spawn(command, args, {detached: true});
            } catch (error) {}

            return spawnedProcess;
        };

        const spawnUpdate = function(args) {
            return spawn(updateDotExe, args);
        };

        const squirrelEvent = process.argv[1];
        switch (squirrelEvent) {
            case '--squirrel-install':
            case '--squirrel-updated':
                // Optionally do things such as:
                // - Add your .exe to the PATH
                // - Write to the registry for things like file associations and
                // explorer context menus

                // Install desktop and start menu shortcuts
                spawnUpdate(['--createShortcut', exeName]);

                setTimeout(app.quit, 1000);
                return true;

            case '--squirrel-uninstall':
                // Undo anything you did in the --squirrel-install and
                // --squirrel-updated handlers

                // Remove desktop and start menu shortcuts
                spawnUpdate(['--removeShortcut', exeName]);

                setTimeout(app.quit, 1000);
                return true;

            case '--squirrel-obsolete':
                // This is called on the outgoing version of your app before
                // we update to the new version - it's the opposite of
                // --squirrel-updated

                app.quit();
                return true;
        }
    }
}

My App.js is below

import * as React from 'react';
import {BrowserRouter, Route, Switch} from 'react-router-dom'
import './Stylesheet.css'
import Dashboard from "./Components/Dashboard";
import Settings from "./Components/Settings";
import './ComponentStyles/BreadcrumbNav.css'
import 'react-toastify/dist/ReactToastify.min.css';
import { ToastContainer, toast } from 'react-toastify';
import CustomerDetails from "./Components/CustomerDetails";

toast.configure({
    position: 'top-center',
    hideProgressBar: true
});
function App() {
  return (

      <BrowserRouter>
          <div>
              <Switch>
                  <Route path="/" render={() => <Dashboard  /> } exact />
                  <Route path="/customer-information/:customer_id" render={(props) => <CustomerDetails {...props} />  } exact />
                  <Route path="/settings" render={() => <Settings /> } exact />
              </Switch>
          </div>
      </BrowserRouter>
  );
}

export default App;

When I look at the chrome console when the app loads I see the following error:

Not allowed to load local resource:
file:///C:/Users/Chris/AppData/Local/MyApp/app-0.1.2/resources/app.asar/index.html

As mentioned above the problem only happens when I launch the electron app when its installed. If I launch it as part of the Node dev server then it works perfectly fine.

UPDATE

Directory structure below as requested

Project Directory Structure

The components directory contains the actual ReactJS components and the directory ComponentStyles are all the individual component stylesheets. The components are typescript so are in tsx format.

2 Answers

After a hell of a lot of trial and error I've managed to get it working finally.

The first thing was to change my App.js so that instead of using BrowserRouter I now use HashRouter as follows and use the history from react-router-dom

import {HashRouter, useHistory} from "react-router-dom";
const history = createBrowserHistory();

<HashRouter basename="/" history={history} >
          <Switch>
              <Route path="/" exact component={Dashboard} />
              <Route path="/customer-information/:customer_id" component={CustomerDetails} />
              <Route path="/settings" component={Settings} />
              <Route path="*" component={NotFound} />
          </Switch>
      </HashRouter>

Then in each component when I want to change location I do the following:

const history = useHistory();
history.push('/newlocation');

When I load the browser window I now have the following:

mainWindow.loadURL(isDev ? windowUrlBase : `file://${__dirname}/../build/index.html`);

And removed the following block

const startUrl = process.env.ELECTRON_START_URL || url.format({
        //pathname: path.join(__dirname, '/../build/index.html'),
        pathname: path.join(__dirname, '../index.html'),
        protocol: 'file:',
        slashes: true,
        webSecurity: false
    });

and isDev is set using the following:

const isDev = require('electron-is-dev');

For the settings menu changing the location this was using IPC using the following:

in electron-start.js

const { ipcMain } = require("electron");

    submenu: [
                    {
                        label: 'Settings',
                        click() {
                            //mainWindow.loadURL(windowUrlBase + "/settings");
                            //history.push('/settings')
                            //location.pathname = "/settings";
                            //ipcMain.send("change-location", "/settings");
                            mainWindow.webContents.send('change-location', '/settings')
                            //mainWindow.location.pathname = "/settings";
                        }
                    }
                ]

Then in a component that is shared across the project I have the following to receive the IPC event

const {ipcRenderer} = window.require('electron')
const history = useHistory();

useEffect(() => {
        ipcRenderer.on('change-location', (event, arg) => {
            history.push(arg);
        });
    })

Correct answer by Boardy on January 28, 2021

Are you sure this line is correct?

const startUrl = process.env.ELECTRON_START_URL || url.format({
        //pathname: path.join(__dirname, '/../build/index.html'),
        pathname: path.join(__dirname, '../index.html'),
        protocol: 'file:',
        slashes: true,
        webSecurity: false
    })

I think the pathname should be path.join(__dirname, './index.html') The double .. gets you two levels above (red), not to the parent folder (orange).

enter image description here

You can test this with a simple file in folders /main/sub/test.js:

const path = require('path')
console.log(__dirname)
console.log(path.join(__dirname, '../'))
console.log(path.join(__dirname, './'))

the output would be:

~/main/sub
~/main/
~/main/sub/

So, in your case, the ../index.html gets your to /crash-catch-control-panel/ but index.html isn't located there.

I am not familiar with using asar, but I think your error message: file:///C:/Users/Chris/AppData/Local/MyApp/app-0.1.2/resources/app.asar/index.html indicates that you are not in the src folder where your index.html file is, but you are looking for index.html one level above it.

Especially because what you have in your response: mainWindow.loadURL(isDev ? windowUrlBase : `file://${__dirname}/../build/index.html`) goes up one level then drops into build. So my concern is that here, you are jumping into your build folder to get the index.html from the build folder. But the build folder is at the same hierarchy as your src folder and you weren't pointed to that.

Answered by Andrew on January 28, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP