Electron.js and React. Build contains huge node_modules

127 Views Asked by At

I'm building a small application with electron.js and react (my first app of this kind). I use electron-packager for builds. The problem is that my builds contain all my node modules. I use my built react app in production though. I thought, maybe I should use separate node_modules for my react part and node part. I mean, in this case it would copy only the packages which are used in node. It's just a few small packages like dotenv.

Here you can see the npm scripts:

 "scripts": {
    "start": "react-scripts start",
    "electron": "electron index.js",
    "release-win": "electron-packager ./ mailer --platform=win32 --arch=x64 --overwrite",
    "release-darwin": "electron-packager ./ mailer --platform=darwin --arch=x64 --overwrite",
    "release-linux": "electron-packager ./ mailer --platform=linux --arch=x64 --overwrite",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  }

That's how the window is created:

function createWindow() {
  // Build desktop window
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true,
      preload: path.join(__dirname, 'preload.js')
    }
  });

  // Add developer tools in dev mode only
  if(isDev)
    mainWindow.webContents.openDevTools()

  // Remove top menu with dev-actions in production
  if(!isDev)
    mainWindow.removeMenu();

  // Load environment variables form .env
  dotenv.config({
    path: path.join(__dirname, './.env')
  });

  // Load url:
  // - In dev it's loaded from localhost - react dev server with hot reloads and other features
  // - In prod it's loads built application
  mainWindow.loadURL(
      isDev
          ? 'http://localhost:3000' // Development server URL
          : `file://${path.join(__dirname, './build/index.html')}` // Production build path
  );

  mainWindow.on('closed', () => {
    mainWindow = null;
  });
}
1

There are 1 best solutions below

0
Arkellys On

Since you use React, and more specifically in your case CRA, you have webpack that can bundle the files for you, and save you from packging node_modules completely. Given this, one solution for your issue is to change the webpack configuration to include your Electron files in the bundle, and then to configure electron-packager to only package the bundled files.

1. Configure webpack to bundle your Electron code

Because they use Node.js, Electron files can't be bundled with the same configuration as for React (browser) files. So to include them in the bundle, you can't just add an entry point, you actually need to add an extra configuration for your main process code (and you can also do the same for the preload, if you have one).

When not ejected, CRA offers little or no possibility of editing the bundler's configuration. So to edit it without ejecting, you can use a tool such as craco or react-app-rewired. I never used Craco, so I can only give you an example of configuration for Rewired:

config-overrides.js

const path = require("path");

module.exports = {
  webpack: (config) => {
    const electronMainConfig = {
      target: "electron-main",
      mode: "production",
      entry: path.join(__dirname, "electron", "main.js"), // Input path
      output: {
        ...config.output,
        filename: "static/js/electron.main.js" // Output path (minus `build`)
      }
    }

    return process.env.NODE_ENV === "development"
      ? config
      : [config, electronMainConfig]
  }
};

On this example, main.js is in electron/main.js and it will be bundled as build/static/js/electron.main.js. In developement, the default original config is used.

The important part is the target: for the main process electron-main can be used, for the preload it would be electron-preload. Unlike the browserslist target (which is used for your renderer, React code), these targets are compatible with the Node.js code of your Electron files.


Note that on build you will get the error:

Cannot read properties of undefined (reading 'publicPath')

This is because CRA was not configured to handle multiple webpack configurations. It will not break the process so it can be ignored (for reference).


Now, to load the app in the window you can use:

app.isPackaged
  ? mainWindow.loadFile(path.join(__dirname, "../../index.html")) // Prod
  : mainWindow.loadURL("http://localhost:3000"); // Dev

With this configuration, all your files should be neatly bundled inside your build folder.

2. Configure electron-packager to only package the bundled files

Now that you have a build folder containing everything you need, you want to tell electron-packager that it should only package this folder (and the package.json, which I'm pretty sure is mandatory). As I gathered, electron-packager only provide an ignore option for the files you don't want to package. In command line, it should look something like:

--ignore=^\\/(?!(build|package\\.json))

I can't test this so I'm not 100% sure about the syntax, but this is based on a configuration shared here.

3. Use the correct main entry point in production

The main entry point is the one specified on your package.json, as required by Electron. In production, you want it to be the path to your main file in the build folder. This part is a little tricky because electron-packager doesn't seem to provide an option to change it while packaging (or I have not found it), so it means you either need to do it manually every time you want to package, or use a script to do it for you. Here is a small script I've used when I needed to do this.

An example of usage with the script in bin/change-entry-point.js, your main file in electron/main.js, the module cross-env and yarn:

"set-entry-point": "node ./bin/change-entry-point.js"
"start": "cross-env ENTRY_PATH=electron/index.js yarn set-entry-point && react-app-rewired start",
"build": "(react-app-rewired build || exit 0)",
"package:win": "cross-env ENTRY_PATH=build/static/js/electron-main.js yarn set-entry-point && yarn build && yarn release-win",
...

The exit 0 on the build script is to prevent the process to stop when it encounters the error I mentioned on step 1.

This command will bundle your files for production and then package them using your command release-win:

yarn package:win

Alternative

If you're looking for a simplier way to package your app without the need to add a custom script, I would suggest to replace electron-packager with electron-builder, this way you can do steps 2 and 3 with the following configuration:

"build": {
  "files": ["./build/**/*"], // What you want to package
  "extraMetadata": {
    "main": "build/static/js/electron.main.js" // Main entry in prod
  },
  "extends": null, // Disables `react-cra` preset to prevent error with paths
  // ...
},

See the official docs for more info on this configuration.