How to display two windows in Electron app with Electron Forge and Vite
Asked Answered
B

2

5

I'm building an Electron JS app using Electron Forge and Vite, and I'd like to display a second window in the application, but it isn't displaying properly. When I run npm run start the second window opens, but it displays the same content as the first window. When I run npm run make it opens the second window still, but the window is empty.

I realize there are lots of documented ways to do this, but most use web pack, or use Electron Forge without Vite.

I've done a bare bones electron forge app and just added a second window. I generated it with the following.

npm init electron-app@latest electron-forge-2-windows -- --template=vite

I have the following directory structure.

.
├── forge.config.js
├── index.html
├── modalWindow.html
├── package.json
├── src
│   ├── index.css
│   ├── main.js
│   ├── preload.js
│   └── renderer.js
├── vite.main.config.mjs
├── vite.modal_renderer.config.mjs
├── vite.preload.config.mjs
├── vite.renderer.config.mjs
└── yarn.lock

forge.config.js

module.exports = {
  packagerConfig: {},
  rebuildConfig: {},
  makers: [
    {
      name: '@electron-forge/maker-squirrel',
      config: {},
    },
    {
      name: '@electron-forge/maker-zip',
      platforms: ['darwin'],
    },
    {
      name: '@electron-forge/maker-deb',
      config: {},
    },
    {
      name: '@electron-forge/maker-rpm',
      config: {},
    },
  ],
  plugins: [
    {
      name: '@electron-forge/plugin-vite',
      config: {
        // `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
        // If you are familiar with Vite configuration, it will look really familiar.
        build: [
          {
            // `entry` is just an alias for `build.lib.entry` in the corresponding file of `config`.
            entry: 'src/main.js',
            config: 'vite.main.config.mjs',
          },
          {
            entry: 'src/preload.js',
            config: 'vite.preload.config.mjs',
          },
        ],
        renderer: [
          {
            name: 'main_window',
            config: 'vite.renderer.config.mjs',
          },
          {
            name: 'modal_window',
            config: 'vite.modal_renderer.config.mjs',
          },
        ],
      },
    },
  ],
};

src/main.js

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

if (require('electron-squirrel-startup')) {
  app.quit();
}

const createWindow = () => {
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
    },
  });

  const modalWindow = new BrowserWindow({
    parent: mainWindow,
    modal: true,
    show: false,
    width: 200,
    height: 200,
  });

  if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
    mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
  } else {
    mainWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`));
  }

  if (MODAL_WINDOW_VITE_DEV_SERVER_URL) {
    modalWindow.loadURL(MODAL_WINDOW_VITE_DEV_SERVER_URL);
  } else {
    modalWindow.loadFile(path.join(__dirname, `../renderer/${MODAL_WINDOW_VITE_NAME}/index.html`));
  }

  modalWindow.show();
};

app.on('ready', createWindow);

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('activate', () => {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow();
  }
});

vite.main.config.mjs

import { defineConfig } from 'vite';

// https://vitejs.dev/config
export default defineConfig({});

vite.renderer.config.mjs

import { defineConfig } from 'vite';

// https://vitejs.dev/config
export default defineConfig({});

vite.modal_renderer.config.mjs

import { defineConfig } from 'vite';
import { resolve } from 'path';

// https://vitejs.dev/config
export default defineConfig({
  build: {
    rollupOptions: {
      input: {
        modal_window: resolve(__dirname, 'modalWindow.html'),
      },
    },
  },
});

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello World!</title>

  </head>
  <body>
    <h1>💖 Hello World!</h1>
    <p>Welcome to your Electron application.</p>
    <script type="module" src="/src/renderer.js"></script>
  </body>
</html>

modalWindow.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello World!</title>

  </head>
  <body>
    <h1>Modal Window</h1>
    <div>Modal</div>
  </body>
</html>

I can see that in my main.js the MODAL_WINDOW_VITE_DEV_SERVER_URL constant is resolving to http://localhost:5174 which seems right, but I don't understand what ties that URL to the correct html file. I'm guessing what I did in vite.modal_renderer.config.mjs is supposed to do that, but clearly I did something wrong.

Brander answered 24/5, 2023 at 2:16 Comment(0)
G
7

The problem with your setup is that a file named modalWindow.html will also be built as modalWindow.html, so your loadURL/loadFile calls will lead to the wrong paths being resolved.

You can do two different things to solve this:

First option

You move your modalWindow.html as index.html into a different directory and set that directory as root in your vite.modal_window.config.js:

mv modalWindow.html modal_window/index.html # directory is of course arbitrary

vite.modal_window.config.js

export default defineConfig({
  root: resolve(__dirname, "modal_window"),
});

Second option

You configure vite.renderer.config.js to build both files and then loadURL/loadFile with the respective names:

vite.renderer.config.js

export default defineConfig({
  build: {
    rollupOptions: {
      input: {
        main_window: resolve(__dirname, "index.html"),
        modal_window: resolve(__dirname, 'modalWindow.html'),
      },
    },
  },
});

main.js

// mainWindow
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
  mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
} else {
  mainWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`));
}

// modalWindow - both use the same constant now
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
  // add file name here ................................. v 
  modalWindow.loadURL(`${MAIN_WINDOW_VITE_DEV_SERVER_URL}/modalWindow.html`); 
} else {
  // change file name here ...................................................... v 
  modalWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/modalWindow.html`));
}

Of the two solutions, I'd strongly recommend the first one (and would also move the main renderer files to a separate directory while at it), even if it requires more directories and config. Edit: See @funerr's answer below for a step-by-step guide on how to do that.

It cleanly separates the builds from each other by treating each renderer as an isolated build without the possibility of accessing URLs outside of the renderer's directory. It also allows different configurations for each renderer, so specific options can be set for one renderer without interfering with the others.

To explain the *_VITE_* constants:

*_VITE_DEV_SERVER_URL is set to the base URL of the Vite dev server started from the renderer configuration http://localhost:<port>/<base>, where port is the first free port starting from 5173 and base is taken from the Vite config (/ by default). The loadURL call will resolve to that URL and serve the index.html by default, as no specific file is given

*_VITE_NAME is set to the renderer's name as defined in forge.config.js. Forge sets Vite's build.outDir to .vite/renderer/<renderer.name>/ for every renderer, so the loadFile call as generated by create-electron-app will resolve to the correct renderer directory.

I hope that helps to clear things up a bit.

Gracioso answered 25/5, 2023 at 3:5 Comment(2)
I tried the first option here but but setting the root the output is the same folder. This means when you build your Electron app, with electron-forge package for example, the built code does not end up in your app.Moradabad
@Moradabad Of course, every renderer's Vite configuration has to point to its own root directory for the build to work. That means you can not build two renderers to, say, "modal_window", but instead have to choose another directory name for one of the renderers.Gracioso
E
2

I've used @TheKvist's first method using the latest vite version.

  1. Create 2 different vite config files:
  • vite.mainwindow.config.ts
  • cite.secondwindow.config.ts

Make them look like so:

import { defineConfig } from "vite";

import path from "path";

// https://vitejs.dev/config
export default defineConfig({
  root: path.join(__dirname, "src", "main"), // Change "main" to your folder on step 4
});

(Corresponds to point 4 dir structure).

  1. Change forge.config.ts to:
 renderer: [
        {
          name: "main_window",
          config: "vite.mainwindow.config.ts",
        },
        {
          name: "second_window",
          config: "vite.secondwindow.config.ts",
        },
      ],
  1. Edit types.d.ts to something like:
declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string;
declare const MAIN_WINDOW_VITE_NAME: string;

declare const SECOND_WINDOW_VITE_DEV_SERVER_URL: string;
declare const SECOND_WINDOW_VITE_NAME: string;

(Mainly to remove squiggly lines in the typescript files)

  1. Create 2 folders under ./src/
./src
│   index.css
│   main.ts
│   preload.ts
│   types.d.ts
│
├───second
│       index.html
│       index.tsx
│       renderer.ts
│       style.css
│
└───main
        index.html
        main.tsx
        renderer.ts

Make sure the renderer.ts has the correct path imports.

Live long and prosper.

Entirety answered 10/11, 2023 at 5:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.