electron-webpack icon indicating copy to clipboard operation
electron-webpack copied to clipboard

How to add multi renderer entries support?

Open erguotou520 opened this issue 7 years ago • 27 comments

I have tried to add electron-webpack.yml

title: true
renderer:
  dll: ['vue']
  webpackConfig: 'custom.webpack.renderer.js'

and the custom.webpack.renderer.js content like this

module.exports = {
    entry: function () {
        return {
            entry1: 'src/renderer/entry1.js',
            entry2: 'src/renderer/entry2.js'
        }
    }
}

, but compile failed

 ERROR in multi (webpack)-dev-server/client?http://localhost:9080 webpack/hot/dev-server entry.js
  Module not found: Error: Can't resolve 'entry1.js' in 'xxx\electron-webpack-quick-start'
   @ multi (webpack)-dev-server/client?http://localhost:9080 webpack/hot/dev-server entry1.js

erguotou520 avatar Jun 25 '18 02:06 erguotou520

@erguotou520 this won't work as the second renderer file isn't being loaded up the the html file that electron-webpack creates. #47 could provide you with some alternatives

walleXD avatar Jul 22 '18 03:07 walleXD

If you want this because you want multiple windows you can achieve that with routing, basically each window will load the same html file, but you'll put a ?route=*** query parameter or something to instruct the renderer on which page/component it should load. Basically you need a router.

fabiospampinato avatar Dec 07 '18 00:12 fabiospampinato

I've published this very simple router for solving the problem: https://www.npmjs.com/package/react-router-static, just add a ?route=*** parameter to the url and you're set.

fabiospampinato avatar Jan 20 '19 20:01 fabiospampinato

The way webpack is configured makes it really difficult to modify the configuration, especially for multiple entry points. A callback would be nice where we could modify or replace options, webpack-merge's smart just isn't enough :(

My workaround for multiple entries: electron-webpack.json:

{
    [...]
    "renderer": {
      "sourceDirectory": "src/renderer",
      "webpackConfig": "webpack.renderer.additions.js"
    }
}

webpack.renderer.additions.js:

const HtmlWebpackPlugin = require('html-webpack-plugin');

const configuration = {
  plugins: [],
  entry: {}
};

const addChunk = (entry, renderer, addHtmlFile) => {
  configuration.entry[entry] = [
    "css-hot-loader/hotModuleReplacement",
    `.\\src\\renderer\\${renderer}`
  ];

  if (addHtmlFile) {
    configuration.plugins.push(new HtmlWebpackPlugin({
      "template": "!!html-loader?minimize=false&url=false!dist\\.renderer-index-template.html",
      "filename": `${entry}.html`,
      "hash": false,
      "inject": true,
      "compile": true,
      "favicon": false,
      "minify": false,
      "cache": true,
      "showErrors": true,
      "chunks": ["theme", entry],
      "excludeChunks": [],
      "chunksSortMode": "auto",
      "meta": {},
      "title": `Chunk ${entry}`,
      "xhtml": false,
      "nodeModules": "node_modules"
    }));
  }
}

addChunk("theme", "theme.ts", false);
addChunk("app", "index.tsx", true);
addChunk("login", "login.tsx", true);

module.exports = configuration;

You have to watch out for styles though, since the MiniCssExtractPlugin options are set to generate a styles.css file. This will crash if you import any styles in more than one entry point. Since we cannot modify the options i added another chunk called theme that contain all my styles. Not really happy with the setup but i guess it works for now.

theuntitled avatar Mar 22 '19 13:03 theuntitled

@theuntitled Would you be willing to share a full example? I'm trying to have multiple renderers, to set as BrowserView in the main BrowserWindow. Isn't clear to me how to load them. Thx!

tvansteelandt avatar May 31 '19 08:05 tvansteelandt

Please also revisit this after the next release - there will be a webpack config callback function, and you'll be able to mutate the config any way you like. Not sure how the rest of the system behaves when there are multiple entry points, but I guess we'll see :)

loopmode avatar May 31 '19 08:05 loopmode

Hi there, I'm trying to use set up PDF.js in my Electron application. The recommended approach is to provide a second entry point for the worker process, which obviously isn't working here. It sounds like the upcoming release may help, but I figured I'd provide a concrete use case.

pvh avatar Jun 09 '19 21:06 pvh

got same problem. does it has a good way to fix it

harwinvoid avatar Jun 18 '19 18:06 harwinvoid

There’s a merged pull request which allows loading multiple entry points but there hasn’t been a release with it yet.

walleXD avatar Jun 18 '19 18:06 walleXD

@walleXD we had a couple of releases these last days. 2.7.0 is out with several merged PRs, plus two fixes up to 2.7.2. I'm not sure which merged PR you are referring to - is it #254 - Support function as custom webpack config?

loopmode avatar Jun 22 '19 19:06 loopmode

@loopmode Yup. That pretty much opens up the door for quite a bit of flexibility @erguotou520 Give the latest update a shot with additional entry points

walleXD avatar Jun 23 '19 17:06 walleXD

Managed to inject a second renderer entry point by using the new feature @loopmode mentioned above. Specifically: the solution below uses a function to customize the default renderer webpack config supplied by electron-webpack. This should be expandable to any number of renderers you might want. It compiles successfully and bundles linked assets as expected.

(Sharing here in case someone ends up here via Google like I did.)

package.json

{
  "name": "foo-bar",
  "electronWebpack": {
    "renderer": {
      "webpackConfig": "webpack.renderer.transformer.js"
    }
  },
  "dependencies": {
    (…)
  }
  (…)
}

webpack.renderer.transformer.js (or whatever you decide to call this)

module.exports = function(context) {
    // Fix filename clash in MiniCssExtractPlugin
    context.plugins.forEach((plugin) => {
        if (plugin.constructor.name === "MiniCssExtractPlugin") {
            plugin.options.filename = '[id].styles.css';
            plugin.options.moduleFilename = (name) => {
                return '[id].styles.css';
            };
        }
    });

    // Add entrypoint for second renderer
    context.entry.secondRenderer = ['./src/renderer/second-renderer.js'];

    return context;
};

Pretty straightforward, this basically boils down to monkeypatching the default webpack config supplied by electron-webpack. As @theuntitled mentioned, the MiniCssExtractPlugin will throw a fit if you import styles in more than one entry point. We fix this by changing the filename format from styles.css to [id].styles.css – this will prefix the filename with a chunk ID.

Quick note: if you previously relied on extending the webpack config by passing an object to electronWebpack.renderer.webpackConfig – for example to bundle images through file-loader – you can't combine that with this method. Instead, you'll have to manually apply the changes you need in the transformation function above.

Note that unlike with the "default" renderer entry point, you won't get a free HTML file out-of-the-box after doing this. I have yet to figure that part out myself, so I'm leaving that as an exercise for the reader. 😉

DriesOeyen avatar Jul 18 '19 20:07 DriesOeyen

"We fix this by changing the filename format from styles.css to [id].styles.css – this will prefix the filename with a chunk ID."

We should do this by default! It won't bother any regular use case, and it will take the pain away from this one.

loopmode avatar Jul 19 '19 05:07 loopmode

Ah, @DriesOeyen and interested reader, the second argument to the config function is the configurator itself. And as far I remember, it has a method for generating the index.html template - with some snooping around the source code, you might be able to reuse it for additional entry templates! But that's a different story and would be worth a separate issue.

Edit: the method is in RendererTarget and that is currently not accessible via the configurator instance. Ideas and PRs welcome!

loopmode avatar Jul 19 '19 05:07 loopmode

@loopmode Thanks for sharing! I ended up rolling my own with html-webpack-plugin, similar to how it's done for the "main" renderer by electron-webpack.

One remaining gotcha I ran into: the "main" renderer's auto-generated HTML page started importing my secondary renderer's scripts and stylesheets. I solved that by specifying the right chunk in the options electron-webpack sets on the HtmlWebpackPlugin it uses internally:

context.plugins.forEach((plugin) => {
    // Ensure other renderers' scripts and styles aren't added to the main renderer
    if (plugin.constructor.name === "HtmlWebpackPlugin") {
        plugin.options.chunks = ['renderer'];
    }
});

Likewise, if you're using another HtmlWebpackPlugin to auto-generate a HTML file for your second renderer you want to use the chunks option to prevent it from importing the scripts and stylesheets of your "main" renderer.

DriesOeyen avatar Jul 19 '19 12:07 DriesOeyen

Yeah good job there! Seems we could do this "properly" out if the box, but we should put some thinking into that first. If we do officially support multiple entries, we need to make sure to delegate the right things to the right place. There might be even more dragons along the way. Maybe after making these experiences you'll be able to provide a PR, or we could work on something together. No hurries, see how it goes, but I'm looking forward to more findings you make.

And BTW, I was thinking about just cloning the entire configured plugin and just adjusting fields like the chunk. That would reuse the currently available logic, including custom template file etc.

Ah. And now that I think about it... If we did support multiple entries..I guess we'd need to rethink some mechanisms like custom template via electronWebpack config - obviously we'd need a way to specify a custom template per entry etc.. sounds complicated.

Maybe the best way is to not really change the current code, but to instead provide a helper function that does exactly what you are doing, on top of the current system..

loopmode avatar Jul 19 '19 15:07 loopmode

@DriesOeyen What i wonder is.. what's your use case? Why do you actually need separate entries? I understand separate entries as useful in web development, but I don't quite understand the necessity in an electron app. I always figured it would be enough to use the very same entry, then pass something to the (invisible) URL, like #entry-a and #entry-b, or ?entry=a and ?entry=b. Then evaluate that in renderer/index.js, and decide whether to include and bootstrap e.g. src/renderer/app-a or src/renderer/app-b.

On the other hand, I've only built a single app with electron, so I'm really curious.

loopmode avatar Jul 20 '19 09:07 loopmode

@DriesOeyen What i wonder is.. what's your use case?

Yeah I also see no practical use for this. Do you just want multiple windows or actually multiple entry points?

The only reason I can think about for having multiple entry points would be to speed up incremental build times if you have a very big app with multiple windows each hosting a lot of logic.

But in most situations you'll be better off just defining a dll bundle to use during development.

fabiospampinato avatar Jul 20 '19 12:07 fabiospampinato

I'm currently experimenting with an app-launcher setup where react-router sits in the root, and there are multiple "apps" for multiple routes, each with a lazy import. I hope I will find some time to stay on it as it looks promising. I wonder at what point performance becomes a factor and what can be done with e.g. DLLs, HardSourceWebpackPlugin, CacheLoader, ThreadLoader etc. (And whether that all is still possible or necessary with the latest webpack). I believe it's worth going that way, rather than the multi entry way.

But then again.. it all depends on the use case.

loopmode avatar Jul 20 '19 19:07 loopmode

Fair points, @loopmode and @fabiospampinato. I'm migrating to electron-webpack from a simpler build toolchain and simply hadn't considered that I could achieve what I needed with a single entry point. Indeed, this turned out to be the superior option since it doesn't require messing with electron-webpack's default behavior.

My use-case is a fairly simple multi-window situation: my app supports 2 kinds of windows. I now load index.html for both BrowserWindows, but pass webPreferences.additionalArguments: ['--second-window'] to the second kind and then load and bind the corresponding UI in the renderer's index.js based on the presence of that flag.

Thanks for the pointers! 🙌

DriesOeyen avatar Jul 21 '19 20:07 DriesOeyen

@DriesOeyen not sure whether you're working with react, but check out the project I started. I'm trying to implement everything I learned from the issues of the last year. https://github.com/loopmode/react-desktop

loopmode avatar Jul 21 '19 20:07 loopmode

@loopmode Sorry, Vue here. 😄

DriesOeyen avatar Jul 22 '19 07:07 DriesOeyen

@fabiospampinato Multiple entry points for renderer processes would be 'nice.'

I am working on project that is not 'view' focused and will have some windows where nodeIntegration is true and other windows where nodeIntegration is false. There are three primary views: a login screen, chat application, administrator panel. I did not implement a SPA framework because it would require additional learning from my team that is familiar with an Asp.Net stack. TypeScript, Electron, Node.js, etc are all new to them. To use electron webpack, I expect I will be putting the html and css files in the static folder to make this work.

The other tasks include 'listeners' and screen recorders that all sit in their own renderer process. There is a lot happening behind the scenes. It would 'nice' to keep it simple by having each html page have it's own js file specific to that html files purpose.

I guess what I am looking for is in the Core Concept section of the electron-webpack documentation would be highlighting that electron-webpack is designed to work with a SPA framework where different windows will route to parts of a single page. It's a fair assumption, but some may be approaching electron-webpack without a SPA.

If using a SPA is the assumption, single renderer entry point is a good choice and maybe add an explanation in the documentation for the reason of a single entry point ( Would that have helped @DriesOeyen?). If it's not the assumption, I would think about changing it.

Appreciating electron-webpack and this discussion!

JonathanBuchner avatar Jul 25 '19 15:07 JonathanBuchner

I've published this very simple router for solving the problem: https://www.npmjs.com/package/react-router-static, just add a ?route=*** parameter to the url and you're set.

@fabiospampinato - just stumbled upon your suggestion, works perfectly for me. Thank you posting it.

atlantisstorm avatar Aug 03 '20 13:08 atlantisstorm

If we are still looking for use-cases then I think the many git hub stars and instant comment activity elicited by this post is proof that people have many use-cases for multi render entries. In those comments, they reference point #3 in this post and I've found this more in-depth and recent post echoing the need for this strategy. If the rest of my post below doesn't have a simpler solution that I haven't found yet then a more in-house and better-documented strategy from electron-webpack then @DriesOeyen above would be of great help. I am not sure how to incorporate @DriesOeyen 's solution into my own project that already has a webpack.renderer.additions.js.

My work's use-case stems from not wanting to rewrite backend express server code for installs of our app that aren't networked to a server or that want the increased physical security of running on an air-gaped(no network connection) machine. We need to give our users a copy of the express server to be run in a hidden electron browser window which gives us a new thread and uses local storage(which is why web workers won't work) instead of a server's database. Many installs won't even have access to a network. We need the extra process/thread spawned by electron when it opens a browser window to offload computationally intensive tasks to a thread that won't hang the UI. We can literally just copy-paste our server code into the index.js file of this new hidden browser window and only have to write a small service to configure the networking/no-networking details. This will reduce our workload by a lot and let us focus on giving the user networking options and pretty UI and graphics things in the App.

We love electron-webpack and have been using it for a year now with a kludge that has webpack start and stops our express server for us. However, it's very kludgy and only set up for development ease. We need a more electron-webpack natural solution that is easier to make an install-package out of and hot-reloading of our server code would be AWESOME too. Otherwise we might need to either drop electron-webpack or make two separate electron-webpack apps. One for UI development and one for server development then glue them together into an install (which sounds like a testing and install nightmare).

I AM NOT AN EXPERT AT ANY OF THIS. I have been clumsily leading my team through all of this and I am only a mid-level dev without even much web-dev experience. I am pretty out-of-my-depth here so if anyone has a 'no-duh' solution I haven't seen please let me know. React routing is not out of the question but I am not sure what that entails or if it will get us our new process/thread to run on (and hot-reloading).

ThisNameNotUsed avatar Nov 11 '20 20:11 ThisNameNotUsed

@DriesOeyen @loopmode I tried to just implement @DriesOeyen solution in my webpack.renderer.additions.js file by converting it to function notation and adding in his css function. I know so little about webpack besides how to get it running for my works project that I hope if I share my webpack.renderer.additions.js file it would be obvious what I am misunderstanding. As said in my post above this one. I just need another window to open and run my server express code:

webpack.renderer.additions.js


//webpack stuff
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
//var DashboardPlugin = require("webpack-dashboard/plugin"); 

//Cesium stuff
const path = require('path'); // The path to the CesiumJS source code
const cesiumSource = 'node_modules/cesium/Source';
const cesiumWorkers = '../Build/Cesium/Workers';
const cesiumMissingImagesFolder = 'static/Images';
const CopywebpackPlugin = require('copy-webpack-plugin');

const { emit } = require('process');


module.exports = function(context){
    //context: __dirname, 
     context.entry.secondRenderer = ['./src/common/server.js'] 
    context.module= {
        unknownContextCritical: false,
        rules: [
            {
                test: /\.(glb|gltf)$/,
                use: 'url-loader'
            }
        ] 
    }
    context.output= {
        // filename: 'index.js',
        // path: path.resolve(__dirname, 'dist'),
        // Needed to compile multiline strings in Cesium
        sourcePrefix: ''
    }
    context.amd= {
        // Enable webpack-friendly use of require in Cesium
        toUrlUndefined: true
    }
    context.node= {
        // Resolve node module use of fs
        fs: 'empty'
    }
    context.resolve= {
        alias: {
            // CesiumJS module name
            cesium: path.resolve(__dirname, cesiumSource)
        }
    }
    context.plugins= [
        //new DashboardPlugin(), // --uncomment after 'yarn add'ing webpack-dashboard for pretty webpack console output
        new HtmlWebpackPlugin({
            template: 'src/index.html'
        }),
        // Copy Cesium Assets, Widgets, and Workers to a static directory,
        new CopywebpackPlugin([ { from: path.join(cesiumSource, cesiumWorkers), to: 'Workers' } ]),
        new CopywebpackPlugin([ { from: path.join(cesiumSource, 'Assets'), to: 'Assets' } ]),
        new CopywebpackPlugin([ { from: path.join(cesiumSource, 'Widgets'), to: 'Widgets' } ]),
        new CopywebpackPlugin([ { from: 'src/renderer/models', to: 'models', flatten: true }]),
        new webpack.DefinePlugin({
            // Define relative base path in cesium for loading assets
            CESIUM_BASE_URL: JSON.stringify('')
        })
        
    ]
    context.plugins.forEach((plugin) => {
        if (plugin.constructor.name === "MiniCssExtractPlugin") {
            plugin.options.filename = 'main.main.css';
            plugin.options.moduleFilename = (name) => {
                return 'main.main.css';
            };
        }
    });
    context.devServer= {
        contentBase: path.join(__dirname, "dist"),
        stats: { 
            colors: true,
            hash: false,
            version: false,
            timings: false,
            //assets: false, 
            //chunks: false,
            //modules: false,
            //reasons: false,
            //children: false,
            //source: false,
            //errors: false,
            //errorDetails: false,
            warnings: false,
            //publicPath: false
          }
    }

    return context;
}

Now nothing really loads in either electron window and new output error: ERROR in ./src/renderer/index.ts 56:8 Module parse failed: Unexpected token (56:8) You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders | var scene = viewer.scene; | class Mission {

data: any;

| tracks: any; | @ multi css-hot-loader/hotModuleReplacement ./src/renderer/index.ts renderer[1]

ThisNameNotUsed avatar Nov 16 '20 01:11 ThisNameNotUsed

Hi @ThisNameNotUsed That's a lot of info, and I must admit that I didn't try to redroduce and I'm not trying to directly follow up (yet). Instead, what I did is I cloned a fresh electron-webpack-quick-start and added a very simple express app to it.

Everything is sketchy, I was just copy-pasting stuff from some tutorials or gists, there's duplication and hard-coded things, but it's a proof of concept.

https://github.com/loopmode/electron-webpack-quick-start/tree/express

Here's essentially the diff to the regular quick start example: https://github.com/loopmode/electron-webpack-quick-start/commit/13a8b4745b22ac3755e6e8f6bb3ae84fdc7ef98c

Some learnings:

  • Creating a renderer process for the express is a good way to go. It's basically a hidden window, and the express app runs in it. So the main/index creates two windows now, the mainWindow and expressWindow. For debuggins, you might use show: true on the express window and use the console and debugger.

  • In order to distinguish the two renderer entries - the usual app and the express app - I use the hash #express in the URL. Thus, the renderer/index.js was moved to renderer/renderer.js (the "old renderer"), and I replaced it by a module that acts as a switch:

// renderer/index.js
if (window.location.hash === '#express') {
    require('./express')
}
else {
    require('./renderer')
}
  • And in main/index.js, when loading up the express window, I specify the hash:
  if (isDevelopment) {
    window.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}/#express`)
  }
  else {
    window.loadURL(formatUrl({
      pathname: path.join(__dirname, 'index.html'),
      protocol: 'file',
      slashes: true,
      hash: 'express'
    }))
  }
  • Using this workflow, you still get nice benefits like hot reloading for the express app itself (not for templates/views, see next).

  • It seemed hard to make the templates work in both development and production build, unless using the __static helper from electron-webpack. However, using that, it became easy.

    • all templates are static assets, so there is no more webpack magic in them..
    • I used app.set("views", path.join(__static, "./express/views")); once
    • Then, in the templates (based on this tutorial, partials are loaded using relative paths
  • Since the renderer itself is running on ELECTRON_WEBPACK_WDS_PORT and express on another, you need to enable cors

  • Here's the express app basically:

(function () {
  "use strict";
  let path = require("path");
  let express = require("express");
  let cors = require("cors");
  let app = express();

  app.use(cors());
  
  app.set("view engine", "ejs");
  app.set("views", path.join(__static, "./express/views"));

  app.get("/", function (req, res) {
    res.render("pages/index", {
      title: "Hello from Express",
      time: new Date().toLocaleString()
    });
  });

  app.get("/about", function (req, res) {
    res.render("pages/about", {
      title: "Another page",
      time: new Date().toLocaleString(),
    });
  });

  app.get("/api/status", function (req, res) {
    res.send({ status: "ok", time: Date.now() });
  });

  let server = app.listen(8080, function () {
    console.log("Express server listening on port " + server.address().port);
  });

  module.exports = app;
})();
  • The "old" renderer app is basically the one from the quick start, however, the two buttons do different things: One makes a fetch to the /api/status endpoint of our express server, which I consider one use case. The other use case and button are to display a page hosted by the express server, using some templating engine (I used ejs here).

  • You'll need some more sophisticated setup for the express port - I simply hard-coded 8080 to get this working.

  • The production app will try to open up a server on that port - not sure how the host OS responds to that (firewalls, security etc). On windows, I was asked by the typical firewall prompt. After giving permission, it worked fine.

You could clone and run the example repo and see if it runs. (Only tested on Windows), You could then add/replicate things and see when exactly it breaks.

loopmode avatar Nov 16 '20 09:11 loopmode