Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Webpack 4 chunking different runtime behaviour compared to Webpack 3 #6626

Closed
garygreen opened this issue Feb 28, 2018 · 17 comments
Closed

Webpack 4 chunking different runtime behaviour compared to Webpack 3 #6626

garygreen opened this issue Feb 28, 2018 · 17 comments

Comments

@garygreen
Copy link

garygreen commented Feb 28, 2018

Do you want to request a feature or report a bug?

Bug / Change in behaviour compared to Webpack 3

What is the current behavior?

When telling Webpack which chunk the runtime should be in, it doesn't appear to change the order of excecution - see below example.

If the current behavior is a bug, please provide the steps to reproduce.

vendor.js

import $ from 'jquery';
console.log('vendor run.');
$.fn.sayHi = function() {
    alert('Hi');
};

main.js

import $ from 'jquery';
console.log('main run.');
$.fn.sayHi();

index.htm

<html>
<body>
<script src="dist/vendor.js"></script>
<script src="dist/main.js"></script>
</body>
</html>

Webpack 3 config

var webpack = require('webpack');
var path = require('path');

module.exports = {

    mode: 'production',

    entry: {
        main: './main.js',
        vendor: './vendor.js'
    },

    output: {
        path: path.resolve(__dirname, './dist'),
        filename: '[name].js',
    },

    optimization: {
        minimize: false,
        runtimeChunk: {
            name: 'vendor'
        },
        splitChunks: {
            cacheGroups: {
                default: false,
                commons: {
                    test: /node_modules/,
                    name: "vendor",
                    chunks: "initial",
                    minSize: 1
                }
            }
        }
    }

};

Webpack 4 Result

image

image

As you can see, main run fires before vendor, even though the runtime has been put inside vendor and it's loaded first.

Webpack 3 Result

image

As you can see vendor run fires first, which means plugin is successfully registered and the alert dialog pops up.

~~

Original issue below - fixed by providing custom splitChunks. Above is now the current issue.

~~
Do you want to request a feature or report a bug?

Bug

What is the current behavior?

Webpack 4 will add jQuery to both vendor.js and main.js when you use ProvidePlugin - this has a side effect that if you have a vendor plugin that is registering itself as a jQuery plugin in vendor.js thru $.fn it is only available for use in vendor.js and won't work in main.js

If the current behavior is a bug, please provide the steps to reproduce.

vendor.js

$.fn.sayHi = function() {
    alert('Hi');
};

main.js

import $ from 'jquery';

$.fn.sayHi();

Webpack 3 config

var webpack = require('webpack');
var path = require('path');

module.exports = {

    entry: {
        main: './main.js',
        vendor: './vendor.js'
    },

    output: {
        path: path.resolve(__dirname, './dist'),
        filename: '[name].js',
    },

    plugins: [
        new webpack.ProvidePlugin({
           $: 'jquery',
           jQuery: 'jquery',
           'window.jQuery': 'jquery',
       }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor'
        })
    ]

};

The webpack 4 config is exactly the same, except minus the CommonChunkPlugin

This is the output:

Webpack 3 with ProvidePlugin

w3 with provideplugin

Webpack 4 with ProvidePlugin

w4 with provideplugin

As you can see, Webpack 4 has added jQuery to both entries, though Webpack 3 hasn't.

What is the expected behavior?

To only include one version of jQuery.

If this is a feature request, what is motivation or use case for changing the behavior?

This is likely down to the new "rules" surrounding when to chunk and it also not playing nicely with the ProvidePlugin. This is something that will likely catch a lot of people out, if they use the default chunking algorithm in Webpack 4 along with ProvidePlugin. Maybe there is a way to default to the old behaviour as Webpack 3, but it's not immediately obvious to me how to do this.

Example Repository

https://github.com/garygreen/webpack4-chunks-prob
~~

@pavelloz
Copy link

pavelloz commented Feb 28, 2018

You removed commonsChunkPlugin, but didnt use optimization.splitChunks as a replacement, thats probably the reason why it doesnt work as expected.

Read more: https://gist.github.com/sokra/1522d586b8e5c0f5072d7565c2bee693

@garygreen
Copy link
Author

garygreen commented Feb 28, 2018

You removed commonsChunkPlugin, but didnt use optimization.splitChunks as a replacement, thats probably the reason why it doesnt work as expected.

Well I've tried various different config options with splitChunks and although it does stop duplicate jQuery and move it to the vendor chunk, the code still doesn't work as expected compared to Webpack 3.

Maybe some futher tweaks to the config would make it work, but in any case I think a lot of people will stumble into this same problem.

    optimization: {
        splitChunks: {
            cacheGroups: {
                default: false,
                commons: {
                    test: /[\\/]node_modules[\\/]/,
                    name: "vendor",
                    chunks: "all"
                }
            }
        }
    },

image

/***/ "./main.js":
/*!*****************!*\
  !*** ./main.js ***!
  \*****************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var jquery__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\");\n/* harmony import */ var jquery__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(jquery__WEBPACK_IMPORTED_MODULE_0__);\n\r\n\r\njquery__WEBPACK_IMPORTED_MODULE_0___default.a.fn.sayHi();\r\n\n\n//# sourceURL=webpack:///./main.js?");

/***/ })

/******/ });
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["vendor"],{

/***/ "./node_modules/jquery/dist/jquery.js":
/*!********************************************!*\
  !*** ./node_modules/jquery/dist/jquery.js ***!
  \********************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

eval(".....JQUERY CODE HERE.......")

/***/ }),

/***/ "./vendor.js":
/*!*******************!*\
  !*** ./vendor.js ***!
  \*******************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

eval("/* WEBPACK VAR INJECTION */(function($) {$.fn.sayHi = function() {\r\n    alert('Hi');\r\n};\r\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\")))\n\n//# sourceURL=webpack:///./vendor.js?");

/***/ })

}]);

@pavelloz
Copy link

pavelloz commented Mar 1, 2018

Well, thats not an code splitting problem anymore. When code is splitted correctly i would shift my focus to this last error - maybe you should use window.jQuery.fn (wild guess, probably wrong).

I migrated my project yesterday and ProvidePlugin didn't make any issues, with config extremely similar to yours:

new webpack.ProvidePlugin({
    $: 'jquery',
    jQuery: 'jquery'
  }),

I would try wrapping the plugin code (vendor.js) into class, exporting it as ES module and try to instantiate it when appropriate. Hope something from above makes positive effect.

@garygreen
Copy link
Author

garygreen commented Mar 1, 2018

Nope. For the life of me I cannot get it working as it did in Webpack 3 - the new splitChunks config settings are cryptic, despite reading https://gist.github.com/sokra/1522d586b8e5c0f5072d7565c2bee693 a million times.

So this is what I've got now:

var webpack = require('webpack');
var path = require('path');

module.exports = {

    mode: 'production',

    entry: {
        main: './main.js',
        vendor: './vendor.js'
    },

    output: {
        path: path.resolve(__dirname, './dist'),
        filename: '[name].js',
    },

    optimization: {
        minimize: false,
        splitChunks: {
            cacheGroups: {
                default: false,
                commons: {
                    test: /jquery/,
                    name: "vendor",
                    chunks: "initial",
                    minSize: 1,
                    reuseExistingChunk: true
                }
            }
        }
    },

    plugins: [
        new webpack.ProvidePlugin({
            '$' : './jquery.js',
           jQuery: './jquery.js'
       })
    ]

};

jquery.js

( function( global, factory ) {
	"use strict";
	if ( typeof module === "object" && typeof module.exports === "object" ) {
		module.exports = global.document ?
			factory( global, true ) :
			function( w ) {
				if ( !w.document ) {
					throw new Error( "jQuery requires a window with a document" );
				}
				return factory( w );
			};
	} else {
		factory( global );
	}
} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {

"use strict";

var
	version = "3.3.1",
	jQuery = function( selector, context ) {
		return new jQuery.fn.init( selector, context );
	};

jQuery.fn = jQuery.prototype = {
};

var
	_jQuery = window.jQuery,
	_$ = window.$;

if ( !noGlobal ) {
	window.jQuery = window.$ = jQuery;
}

return jQuery;
} );

(this is a split down version for easier testing)

This gives me a runtime in main.js and not in vendor.js - however in Webpack 3 it was the other way round, runtime was in vendor.js and not in main.js.

After some debugging it looks like main.js is executing BEFORE vendor.js which is why the plugins aren't available, as they haven't been attached yet because vendor hasn't run.

I've tried fiddling around with the new optimization.runtimeChunk config option to see if I can get the vendor.js with the runtime instead, but that option doesn't seem to have any effect whatsoever.

I know Webpack 4 is in it's early days - but looking at the comments on that Gist I'm not the only one having trouble migrating.

@pavelloz you mention you had no troubles with migrating - do you use jQuery and any plugins (and chunk to vendor.js) by any chance? Have you tried just creating a very basic setup where you have a) jQuery, b) some jQuery plugin, c) Use the jQuery plugin in main.js and have jQuery and the plugin go to vendor.js ? That's essentially the problem I'm experiencing at the moment.

@garygreen
Copy link
Author

garygreen commented Mar 1, 2018

Ok I've managed to get the runtime to go to a seperate file by doing:

    optimization: {
        runtimeChunk: true
....

This creates a runtime~main.js file however that still runs BEFORE vendor.js and so the plugins aren't attached yet. I somehow need to create a vendor-main runtime, or even better... have the runtime just go in vendor like it did in Webpack 3.

Webpack 3

webpack3

Webpack 4

webpack4

@pavelloz
Copy link

pavelloz commented Mar 1, 2018

I know its cryptic ;)

Anyway, i think your webpack config looks suspicious, the things i mentioned and now...

new webpack.ProvidePlugin({
            '$' : './jquery.js',
           jQuery: './jquery.js'
       })

I have never seen anyone using ProvidePlugin that way ('./jquery.js')

@garygreen
Copy link
Author

garygreen commented Mar 1, 2018

Anyway, i think your webpack config looks suspicious, the things i mentioned and now... I have never seen anyone using ProvidePlugin that way ('./jquery.js')

That's a perfectly valid way to use the plugin. Also I only did it that way to try to understand / debug what's happening easier with Webpack rather than pulling down the whole of jQuery and looking at huge build outputs. If you shim jQuery library itself, you get the same result.

@Sebazzz
Copy link

Sebazzz commented Mar 1, 2018

I am having the same issue. The gist of the issue is that you'd expect the common chunk to contain the runtime, as it contains the dependencies of the other chunks, but instead the dependent code contains the runtime chunk.

Since it is a complex subject, and is black magic for the uninitiated, the best solution would IMHO be an option to be able to specify in which output(s) the runtime should be placed. Or consider it a bug as it does follow behavior essentially preventing this (imho) common code splitting scenario.

Fairly large example for a SPA / Smaller example of multiple-pages/entry points.

@garygreen garygreen changed the title Webpack 4 chunking different behaviour when combined with ProvidePlugin as compared to Webpack 3 Webpack 4 chunking different runtime behaviour compared to Webpack 3 Mar 1, 2018
@garygreen
Copy link
Author

So it seems you can change where the runtime goes by naming the entry:

    optimization: {
        runtimeChunk: {
            name: 'vendor'
        },

However in my case this doesn't change what order the files are executed in, it seems main.js is still firing before vendor.js, even though runtime is in vendor.

@garygreen
Copy link
Author

I'm opening a new issue as this one went off track a bit at the start - #6647

@Legends
Copy link
Member

Legends commented Mar 14, 2018

Continuing off track..

Multi-Page-Application

I have a question regarding "controlling the bundle output using webpack.config.js"

With webpack v3 we used CommonsChunkPlugin, worked good for me.
With webpack 4 we have to configure this with optimization.splitchunks right?!

What I want:

I have a vendor.build.js which imports all vendor libs, output should be: vendor.bundle.js.
Inside of vendor.build.js I also import all css/scss/less files which want to be outputed as: bundle.css.

In the webpack.config I use one instance of the ExtractTextPlugin for extracting all css/scss/less into one file.

I also load modules dynamically in vendor.js and postome.js.

 entry: {
        "vendor": [join(scriptsfolder, "vendor.build")],  // jquery/boostrap/lodash...
        "view.default.index": join(scriptsfolder, "controllers/default/index"),  // page script
        "view.default.posttome": join(scriptsfolder, "posttome")     // page script
    },

How do I have to configure the optimization config part to get the following results in dist folder:

  • vendor.bundle.js
  • bundle.css
  • postome.js
  • view.default.index.js
  • AsynchronouslyLoadedModule1.js
  • AsynchronouslyLoadedModule2.js

@Legends
Copy link
Member

Legends commented Mar 15, 2018

Can someone setup a gist with examples for common configurations using SPA and MPA using the new way of configuring bundling?

And the bundle.css is always empty

When I remove the optimization part completely I get: (seems a lot of duplicates)

image

@Legends
Copy link
Member

Legends commented Mar 29, 2018

The CommonsChunkPlugin has been removed in v4.
You better open a new issue, this thread is dead.

Read this and tell me if it solves your problem.

@Legends
Copy link
Member

Legends commented Mar 29, 2018

I guess, the main problem is, that we still think the CommonsChunk way and the documentation it rather sparse regarding this new approach...

@message
Copy link

message commented Apr 3, 2018

@Legends what did you use to make usage analytics data here?

@Legends
Copy link
Member

Legends commented Apr 4, 2018

@message webpack-bundle-analyzer

@ioanungurean
Copy link

ioanungurean commented Apr 4, 2018

Hi, I had the same issues but I managed to fix it. Here you have a Webpack 4 SPA build configuration example, maybe it will help you. And this is the commit when I migrated from Webpack 3 to Webpack 4.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants