javascriptnode.jswebpackelectron

How do I get a javascript object from the Main process to the Renderer process in electron + webpack


I'm building an electron + webpack application. The result I'm trying to achieve is to load "config.yaml" as a javascript object in the main process (I think it has to, because renderer doesn't have access to node's "fs") and then use IPC to move it over to renderer where I actually need it. The file structure looks like this:

- .webpack/
- node_modules/
- src/
+-- config.js
+-- header.js
+-- index.css
+-- index.html
+-- main.js
+-- preload.js
+-- renderer.js
+-- theme.js
- static/
+-- cfg/
  +-- config.yaml
+-- themes/
  +-- ...
- .gitignore
- forge.config.js
- package-lock.json
- package.json
- webpack.main.config.js
- webpack.renderer.config.js
- webpack.rules.js

main.js

const { app, BrowserWindow, Menu, ipcMain } = require('electron');
const fs = require('fs');
const yaml = require('yaml');
const theme = require('./theme.js');

// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (require('electron-squirrel-startup')) {
  app.quit();
}

let config = fs.readFileSync('./static/cfg/config.yaml', 'utf8');
config = yaml.parse(config);

const createWindow = () => {
  // Create the browser window.
  const mainWindow = new BrowserWindow({
    width: config.cl_win_width,
    height: config.cl_win_height,
    webPreferences: {
      preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
    },
  });

  // Remove the menu bar
  Menu.setApplicationMenu(null);

  // and load the index.html of the app.
  mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);

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

// 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.whenReady().then(() => {

  // Config
  ipcMain.handle('config', () => {
    return config;
  })

  // Theme API
  ipcMain.handle('get-themes', () => {
    const fileList = fs.readdirSync('./static/themes');
    let themeList = theme.getThemes(fileList);
    return themeList;
  })

  createWindow();

  // 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.
  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) {
      createWindow();
    }
  });
});

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

preload.js

// See the Electron documentation for details on how to use preload scripts:
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
const { ipcRenderer, contextBridge } = require('electron');
const header = require('./header.js');

contextBridge.exposeInMainWorld('mainAPI', {
    getConfig: () => ipcRenderer.invoke('config'),
});

contextBridge.exposeInMainWorld('themeAPI', {
    listThemes: () => ipcRenderer.invoke('get-themes'),
});

contextBridge.exposeInMainWorld('headerAPI', header);

renderer.js

import './index.css';

let headerAPI = window.headerAPI;

// Load config
let config;
window.mainAPI.getConfig().then(res => {
    console.log(res);
    config = res;
});

// Test this
console.log(config);

const content = document.querySelector('#content');

// Create Header
headerAPI.createHeader(content);

config.js

export async function getConfig() {
    // Makes call to main process and returns the config object
    let config = await window.mainAPI.getConfig();
    console.log(config);
    return config
}

Webpack is configured to use file-loader for my static files.

When I run this application with npm run, electron-forge . is executed, webpack compiles my code, and starts a dev server. The application window loads.

I expected to see the dev tools console display a message containing the javascript object logged out. This is what I see instead:

undefined                        renderer.js:19

{my javascript object}           renderer.js:14

In main, console logging the "config" object shows that it does load correctly.

In renderer, it logs correctly on line 14. My understanding is that because we wait for the async function to return before we log, line 14 should execute before line 19. An option I've considered is that ".then()" doesn't stop the script from executing, which is why this error is occurring.

My question is this:

How do I get the config object from main over to renderer, and wait for it to be loaded before I proceed executing the rest of that script (mind, there's going to be over 6000 lines of code, so I've nixed the disgusting idea of putting everything in the .then() scope).

Just a thought: I had done this before on this project's previous iteration. I managed it simply by not using main to load the config, and instead had config.js use const fs = require('fs');, and defined a function loading it there, and then used preload to expose that. That no longer works here, because nothing except main.js can access fs now. I really have no idea how to proceed.

If anyone can help me understand what I'm missing here, and how to fix this issue, I would be most grateful. Thanks!


Solution

  • In the code below, execution doesn't wait for the Promise returned from window.mainAPI.getConfig() to be resolved before continuing execution. The callback that is passed into the then() function is executed asynchronously, and that's why you're second console.log() statement is invoked first.

    let config;
    window.mainAPI.getConfig().then(res => {
        console.log(res);
        config = res;
    });
    
    console.log(config);
    

    There are a number of options to work around this, other than putting all the rest of your code in the then() callback. For your specific case, the most convenient approach is to make use of a top level await statement (ES2022) to wait for the Promise to be resolved before continuing execution. Fortunately this is available in Node.js ESM, and thus can be used in Electron:

    const config = await window.mainAPI.getConfig();
    console.log(config);