Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Multiple entry points -> multiple html outputs? #218

Closed
kirbysayshi opened this issue Feb 11, 2016 · 26 comments
Closed

Multiple entry points -> multiple html outputs? #218

kirbysayshi opened this issue Feb 11, 2016 · 26 comments

Comments

@kirbysayshi
Copy link

Hello! I have kind of a specific use case, and was hoping to get some feedback on if html-webpack-plugin is the right tool for this job. Also, I'm relatively new to webpack so I might be missing something.

I have a webpack config similar to this:

module.exports = {
  entry: {
    'page1: './apps/page1/scripts/main.js',
    'page2': './apps/page2/src/main.js',
  },
  output: {
    path: __dirname,
    filename: "apps/[name]/build/bundle.js"
  }
}

This generates a bundle.js in a build folder for each "app". I tried using html-webpack-plugin to also generate an html page for each app (notice the filename placeholder similar to the output key):

plugins: [
  new HtmlWebpackPlugin({
    filename: 'apps/[name]/build/index.html'
  })
]

But that fails with the following error:

node_modules/.bin/webpack --progress -v
 70% 11/11 build modulesnode_modules/html-webpack-plugin/lib/compiler.js:86
          content: childCompilation.assets[outputOptions.filename].source()
                                                                  ^
TypeError: Cannot read property 'source' of undefined
    at node_modules/html-webpack-plugin/lib/compiler.js:86:67
    at Compiler.<anonymous> (node_modules/webpack/lib/Compiler.js:214:10)
    at node_modules/webpack/lib/Compiler.js:403:12
    at Compiler.next (node_modules/webpack/node_modules/tapable/lib/Tapable.js:67:11)
    at Compiler.<anonymous> (node_modules/webpack/lib/CachePlugin.js:40:4)
    at Compiler.applyPluginsAsync (node_modules/webpack/node_modules/tapable/lib/Tapable.js:71:13)
    at Compiler.<anonymous> (node_modules/webpack/lib/Compiler.js:400:9)
    at Compilation.<anonymous> (node_modules/webpack/lib/Compilation.js:577:13)
    at Compilation.applyPluginsAsync (node_modules/webpack/node_modules/tapable/lib/Tapable.js:60:69)
    at Compilation.<anonymous> (node_modules/webpack/lib/Compilation.js:572:10)
    at Compilation.applyPluginsAsync (node_modules/webpack/node_modules/tapable/lib/Tapable.js:60:69)
    at Compilation.<anonymous> (node_modules/webpack/lib/Compilation.js:567:9)
    at Compilation.applyPluginsAsync (node_modules/webpack/node_modules/tapable/lib/Tapable.js:60:69)
    at Compilation.<anonymous> (node_modules/webpack/lib/Compilation.js:563:8)
    at Compilation.applyPluginsAsync (node_modules/webpack/node_modules/tapable/lib/Tapable.js:60:69)
    at Compilation.seal (node_modules/webpack/lib/Compilation.js:525:7)
    at Compiler.<anonymous> (node_modules/webpack/lib/Compiler.js:397:15)
    at node_modules/webpack/node_modules/tapable/lib/Tapable.js:103:11
    at Compilation.<anonymous> (node_modules/webpack/lib/Compilation.js:445:10)
    at node_modules/webpack/lib/Compilation.js:417:12
    at node_modules/webpack/lib/Compilation.js:332:10
    at node_modules/async/lib/async.js:52:16
    at done (node_modules/async/lib/async.js:246:17)
    at node_modules/async/lib/async.js:44:16
    at node_modules/webpack/lib/Compilation.js:332:10
    at node_modules/async/lib/async.js:52:16
    at done (node_modules/async/lib/async.js:246:17)
    at node_modules/async/lib/async.js:44:16
    at node_modules/webpack/lib/Compilation.js:332:10
    at node_modules/async/lib/async.js:52:16

I assume that html-webpack-plugin is only ever planning on generating one output file. Is there a configuration option I'm missing, or perhaps an easy modification, to make the plugin generate one file for each entry point?

Thanks!

@simshanith
Copy link

@kirbysayshi - this is possible via multiple instances of the HtmlWebpackPlugin. You will need to configure each instance. You probably want to specify the related chunks to include as well as disable injecting all assets.

module.exports = {
  entry: {
    'page1': './apps/page1/scripts/main.js',
    'page2': './apps/page2/src/main.js'
  },
  output: {
    path: __dirname,
    filename: "apps/[name]/build/bundle.js"
  },
  plugins: [
    new HtmlWebpackPlugin({
      inject: false,
      chunks: ['page1'],
      filename: 'apps/page1/build/index.html'
    }),
    new HtmlWebpackPlugin({
      inject: false,
      chunks: ['page2'],
      filename: 'apps/page2/build/index.html'
    })
  ]
};

There may be a better way, but my team uses a similar approach to generate multiple pages with different assets.

@kirbysayshi
Copy link
Author

@simshanith thanks for the reply! That's basically exactly what I wanted. Thanks for the clear answer!

I enabled inject just to see what would happen. It injected the right bundle.js, but with a relative path that implied html-webpack-plugin was assuming the index.html would only exist in the project root. Is that expected?

@simshanith
Copy link

You probably want to look at the output.publicPath setting. https://github.com/webpack/docs/wiki/configuration#outputpublicpath

@kirbysayshi
Copy link
Author

Thanks! That's exactly what I was looking for. Sorry about asking here for that, since I'm still new to webpack it's tough to know what's solved at the webpack level vs the plugin/loader level.

@yorkie
Copy link

yorkie commented Dec 6, 2016

@simshanith's solution is working but the inject is to be true, thanks :)

@frontEnd-fucker
Copy link

@yorkie inject set to 'true' is work for me

@SerendpityZOEY
Copy link

SerendpityZOEY commented Dec 16, 2016

@simshanith
Hi, thank you for your code snippet, it's clear and simple. I met an error when serving two pages in react. One of the page works well but another page is always throws an error:
Uncaught Error: _registerComponent(...): Target container is not a DOM element.

And this is my configuration for webpack:

    entry: {
        'page1': './src/components/app.js',
        'page2': './src/page2/main.js'
    },
    output: {
        path: path.join(__dirname, './public/js/'),
        filename: 'bundle.js',
        publicPath: '/js/'
    },
    module: {
        loaders: [{
            test: /.jsx?$/,
            exclude: /node_modules/,
            loader: 'babel',
            query: { presets: ['react', 'es2015'],
                plugins: ['recharts']}
        },
            {
                test: /\.css$/,
                loader: 'style!css?modules',
                include: /flexboxgrid/,
            }]
    },
    plugins: [
        new HtmlWebpackPlugin({
            inject: false,
            chunks: ['page1'],
            filename: 'public/index.html'
        }),
        new HtmlWebpackPlugin({
            inject: false,
            chunks: ['page2'],
            filename: 'public/test.html'
        })
    ]
};

@simshanith
Copy link

@SerendpityZOEY - LMGTFY

Not a problem with the plugin at all; rather when your template injects its code http://stackoverflow.com/questions/26566317/invariant-violation-registercomponent-target-container-is-not-a-dom-elem

@enepomnyaschih
Copy link

I'd rather reopen this issue, because the original proposal is much clearer and shorter compared to a workaround used by the plugin. It's blunder that tags like [name], [hash], [chunkhash] are unavailable in plugin's filename configuration.

@ghost
Copy link

ghost commented Feb 10, 2017

thx~

@AlesRFK
Copy link

AlesRFK commented Feb 12, 2017

@enepomnyaschih When you have like 5.html pages you still need to type 5 plugins right?

@enepomnyaschih
Copy link

@AlennGK Exactly. At this point, my webpack config looks kind of ugly:

var titles = {
    index: "Home",
    about: "About us",
    blog: "Blog"
};
module.exports = {
    entry: {
        index: "./index.js",
        about: "./about.js",
        blog: "./blog.js"
    },
    plugins: Object.keys(titles).map(function(id) {
        return new HtmlWebpackPlugin({
            chunks: ["common", id],
            filename: id + ".html",
            template: "!!html-webpack-plugin/lib/loader.js!./templates/" + id + ".html",
            inject: "body",
            title: titles[id]
        });
    }).concat([
        new webpack.optimize.UglifyJsPlugin({
            minimize: true
        })
    ])
    // ... other options
}

Simple plugin array gets complicated by Object.keys, map and concat calls. The next config would look much prettier in my opinion:

var titles = {
    index: "Home",
    about: "About us",
    blog: "Blog"
};
module.exports = {
    entry: {
        index: "./index.js",
        about: "./about.js",
        blog: "./blog.js"
    },
    plugins: [
        new HtmlWebpackPlugin({
            chunks: ["common", "[name]"],
            filename: "[name].html",
            template: "!!html-webpack-plugin/lib/loader.js!./templates/[name].html",
            inject: "body",
            title: function(id) {
                return titles[id];
            } // or maybe a shorthand like title: titles?
        }),
        new webpack.optimize.UglifyJsPlugin({
            minimize: true
        })
    ]
    // ... other options
}

@simshanith
Copy link

simshanith commented Feb 13, 2017

@enepomnyaschih - re: reopening this issue - a workaround has been provided. Changes to the options would be a breaking change and should be handled as a separate feature request.

Re: your proposed [name] solution - while a nicer syntax, it limits the capabilities of the plugin to assume one-to-one entry point to HTML template relationships. More complex relationship, e.g. one template with multiple entry points, specific templates for specific entries, etc. are poorly represented with your proposal.

I'd recommend abstracting your titles to HtmlWebpackPlugin reducer into a utility function to simplify your "kind of ugly" config.

@enepomnyaschih
Copy link

@simshanith - I'm fine with opening a separate feature request. However, if you leave a possibility to instantiate plugin multiple times, then it is not a breaking change - it doesn't break existing configs. It is fully compatible.

My proposal is just a proposal - of course, it doesn't cover all use cases. It is open for discussion. To make it more flexible, the next approaches could be applied:

    new HtmlWebpackPlugin({
        filename: "[name].html",
        template: "!!html-webpack-plugin/lib/loader.js!./templates/[name].html", // default
        entries: { // overrides
            index: {
                template: "!!html-webpack-plugin/lib/loader.js!./templates/base.html"
            }
        }
    })

or

    new HtmlWebpackPlugin({
        filename: "[name].html",
        template: function(entry) {
            // ok, this is even uglier, but this is an exceptional use case, right?
            var template = (entiry === "index") ? "base" : entry;
            return "!!html-webpack-plugin/lib/loader.js!./templates/" + template + ".html";
        }
    })

or simply use your current approach to define multiple plugin instances.

I think that these cases are rare. What I proposed initially works fine in the majority of use cases.

Utility function is not a solution, because it will still force users to merge the plugins via "concat" method.

I just want to emphasize that it is not common for WebPack plugins to be instantiated multiple times just to cover several entries. It is confusing.

Please let me know if I should open a separate feature request.

@jantimon
Copy link
Owner

@enepomnyaschih the problem is rather the compilation and cache which is used right now.

In order to generate multiple files it would have to be refactored a lot.

@liujingbreak
Copy link

liujingbreak commented Feb 13, 2017

Currently this plugin is treating multiple entry page application as single entry chunk with vendor chunks application, I don't feel comfortable with solution of "instantiate plugin multiple times" either.

Webpack does provide information like compilation.entrypoints in each of what is array of "initial" chunk, these chunks and the relationship between initial chunks should be the information for this plugin to calculate and insert proper <script> for each entry page rather than simply grab all entry chunks for an HTML page.

Of course we can exclude some chunks manually, but that's weird, there is very good calculated information from Webpack already.

@JerryC8080
Copy link

@simshanith thanks, inject set to true is work to me too.

@maoshuai
Copy link

4. webpack Dev Server should be configed too.

rewrite urls
/config/webpackDevServer.config.js:

    historyApiFallback: {
      disableDotRule: true,
      // 指明哪些路径映射到哪个html
      rewrites: [
        { from: /^\/admin.html/, to: '/build/admin.html' },
      ]
    }

best practice: http://imshuai.com/create-react-multiple-enties/

@gwuhaolin
Copy link

@kirbysayshi you can try this:

web-webpack-plugin

auto detect html entry demo

AutoWebPlugin plugin can find all page entry in an directory, then auto config an WebPlugin for every page to output an html file, you can use it as below:

webpack config

const autoPlugin = new AutoWebPlugin(
	// the directory hold all pages
	'./src/pages', 
	{
	// all props below is not required  
	  
	// {string,function}
	// the template file path used by all pages
	// if typeof template ===string: template config is html template file full path
	// if typeof template ===function: template config is function(pageName)=>newFullPath ,ask user for detail
	template: './src/template.html',
	
	// {string,function}
	// javascript main file for current page,if it is null will use index.js in current page directory as main file
	// typeof entry===string: entry config is entry file full path
	// typeof entry===function: entry config is function(pageName)=>newFullPath ,ask user for detail
	entry: null,
	
	// {function}
	// get WebPlugin output filename,default filename is pageName
	// set filename as function(pageName)=>filename to add custom logic 
	filename:null,
	
	// CommonsChunkPlugin options for all pages entry find by AutoWebPlugin.
	// if this is null will not do commonsChunk action
	commonsChunk: {
	    name: 'common',// name prop is require,output filename
	    minChunks: 2,// come from CommonsChunkPlugin
	},
	
	// {Array} pre append to all page's entry
	preEntrys:['./path/to/file1.js'],
	
	// {Array} post append to all page's entry
	postEntrys:['./path/to/file2.js'],
	
	// {string} publicPath for css file,for js file will use webpack.publicPath
	stylePublicPath:null,
	
	// page name list will not ignore by AutoWebPlugin(Not output html file for this page name)
	ignorePages:['pageName'],
	
	// whether output a pagemap.json file which contain all pages has been resolved with AutoWebPlugin in this way:
	// {"page name": "page url",}
	outputPagemap: true,
	}
);

module.exports = {
	// AutoWebPlugin will generate a entry for every page find in the directory hold all pages
	// autoPlugin.entry({}) used to pass entrys find by AutoWebPlugin to webpack config
	entry: autoPlugin.entry({
		youAdditionalEntryName: 'you additional entry path',
	}),
};

src directory

── src
│   ├── home
│   │   └── index.js
│   ├── ie_polyfill.js
│   ├── login
│   │   └── index.js
│   ├── polyfill.js
│   ├── #
│   │   └── index.js
│   └── template.html

output directory

├── dist
│   ├── common.js
│   ├── home.html
│   ├── home.js
│   ├── ie_polyfill.js
│   ├── login.html
│   ├── login.js
│   ├── polyfill.js
│   ├── #.html
│   └── #.js

AutoWebPlugin find all page home login # directory in ./src/,for this three page home login # will use index.js as main file and output three html file home.html login.html #.html

@york-xtrem
Copy link

@enepomnyaschih
The code you suggest is fine. But the job of introducing every page you create should be automated. The webpack configuration should only be written at the beginning of the project, otherwise the automated philosophy will be broken.

@gwuhaolin
AutoWebPlugin is great. But it would really be interesting something simpler to set up and use.

Have your directory of pages already created, without needing a common template.
Because of that work has already been done a template loader.
As for example:

Then simply have the same behavior as html-webpack-plugin has default for a single page.

It is very difficult to configure webpack for static pages / multi pages... something that is really trivial with Gulp.

@schmod
Copy link

schmod commented Sep 14, 2017

Should we incorporate @simshanith's advice into the docs?

schmod pushed a commit to schmod/html-webpack-plugin that referenced this issue Sep 14, 2017
Improves README.md to mention the `chunks` option in sections that might
be of interest to users who have multiple entry-points and/or HTML
files.

Also adds a new example demonstrating how multiple entry points AND
multiple pages may be used together, alongside the CommonsChunkPlugin.

Based on an example written by @simshanith in jantimon#218.
@go2sujeet
Copy link

go2sujeet commented Feb 17, 2018

Apologies for commenting on a closed thread, webpack newbee here.
Please don't judge.

Is there a programmatic way of generating the .html file like (home.html, settings.html,dashboard.html)?
I have multiple entries in the input, this is for a non SPA project

Thanks in advance :)

@paulcalabro
Copy link

@sujeetbuddiga AFAIK, you have to have multiple instantiations of the plugin, each with a separate config.

@askoretskiy
Copy link

I think this topic is bigger than @kirbysayshi explained.

I have exactly the situation @liujingbreak shown.

I have multiple entries and webpack splitChunks plugin. What this plugin does is create as much chunks as needed. The name of the chunk is generated on fly.

So I cannot simply provide exact list of chunks for HtmlWebpackPlugin to use. Even if I guess, while application grows -- it would change.

So there should be a way to provide entry name so list of chunks needed are pulled from webpack runtime.

@umeboshi2
Copy link

@askoretskiy It looks like the chunks can be easily pulled from the "entrypoints" property of the stats. I have a simple patch here which seems to work for me: #944

@lock
Copy link

lock bot commented Jun 3, 2018

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@lock lock bot locked as resolved and limited conversation to collaborators Jun 3, 2018
# for free to subscribe to this conversation on GitHub. Already have an account? #.
Labels
None yet
Projects
None yet
Development

No branches or pull requests