electron-userland / electron-webpack

Scripts and configurations to compile Electron applications using webpack
https://webpack.electron.build/
906 stars 171 forks source link

How to add multi renderer entries support? #165

Open erguotou520 opened 6 years ago

erguotou520 commented 6 years ago

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
walleXD commented 6 years ago

@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

fabiospampinato commented 5 years ago

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 commented 5 years ago

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.

theuntitled commented 5 years ago

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.

tvansteelandt commented 5 years ago

@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!

loopmode commented 5 years ago

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 :)

pvh commented 5 years ago

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.

harwinvoid commented 5 years ago

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

walleXD commented 5 years ago

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

loopmode commented 5 years ago

@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?

walleXD commented 5 years ago

@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

DriesOeyen commented 5 years ago

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. 😉

loopmode commented 5 years ago

"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 commented 5 years ago

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!

DriesOeyen commented 5 years ago

@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.

loopmode commented 5 years ago

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 commented 5 years ago

@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.

fabiospampinato commented 5 years ago

@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.

loopmode commented 5 years ago

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.

DriesOeyen commented 5 years ago

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! 🙌

loopmode commented 5 years ago

@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

DriesOeyen commented 5 years ago

@loopmode Sorry, Vue here. 😄

JonathanBuchner commented 5 years ago

@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!

atlantisstorm commented 4 years ago

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.

ThisNameNotUsed commented 4 years ago

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 commented 4 years ago

@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]

loopmode commented 4 years ago

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:

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.