webpack: Webpack 4 splitChunks causes reused chunks to not be executed

Bug report

I am working in a complicated app that has a bunch of bundles on every page. Most of these are “global” bundles and appear on every page. So, I have configured webpack to prevent any modules that appear in these bundles from appearing in any other bundle via splitChunks. More context on this here: https://stackoverflow.com/questions/49163684/how-to-configure-webpack-4-to-prevent-chunks-from-list-of-entry-points-appearing

What is the current behavior?

It seems that when I configure splitChunks in this way, it causes the global bundles to not be executed.

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

Here’s a branch on a repo that reproduces this issue: https://github.com/lencioni/webpack-splitchunks-playground/tree/splitchunks-execution-problem

In this repro case, it currently logs the following to the console:

core module
core module b
non-core module
non-core b2
non-core module b

In the debugging I’ve done, it seems that this splitChunks configuration causes the executeModules part of the bundle that the runtime chunk is looking for to be undefined.

What is the expected behavior?

The code in the “global” bundles should be executed, which should cause the console log to look more like this:

core module
core bundle
core module b
core bundle b
core bundle c
non-core module
non-core b2
non-core module b

Other relevant information: webpack version: 4.8.1 Node.js version: 8.9.1 Operating System: Mac Additional tools:

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 33
  • Comments: 26 (3 by maintainers)

Commits related to this issue

Most upvoted comments

This seems to maybe be happening intentionally here: https://github.com/webpack/webpack/blob/2986ab1a18e895ac3f2546ac0c70bf73c0fdb679/lib/optimize/SplitChunksPlugin.js#L559-L565

However it isn’t clear to me from the code or the commit message (added by https://github.com/webpack/webpack/commit/18ae73dad4bb761593a9f5529a182601e1b57520) why this behavior exists.

If I disable this entrypoint removal, I get the build that I expect and my program runs as expected.

It seems that this logic should either be removed or perhaps there should be an option added to cacheGroups that allows a cacheGroup to also be an entrypoint.

@IanVS here you go:

// =========
// site/common-entry.js
import session from './common/global-session';
import initHeader './common/header';

initHeader(session);

// =========
// site/page1-entry.js
import session from './common/global-session';
// ... do some page-specific stuff here

// =========
// html site/page1
<script src="/site/common.js">
<script src="/site/common-entry.js">
<script src="/site/page1-entry.js">

// =========
// html other/other2
// no site/common here - it's not relevant in the context of "other/" area
<script src="/other/other2-entry.js">

// =========
// finally, webpack config:

entry: {
	'site/common-entry': 'root/site/common-entry.js',
	'site/page1-entry': 'root/site/page-entry1.js',
	'other/other2-entry': 'root/other/other-entry.js',
}
optimization: {
	runtimeChunk: {
		name: 'runtime',
	},
	splitChunks: {
		cacheGroups: {
			common: {
				name: 'site/common',
				test: (module, chunks) => {
					const names = chunks
						.map(c => c.name)
						.filter(Boolean);

					const usedByCommonEntry = names.some(name => name === 'site/common-entry');
					return usedByCommonEntry;
				},
				chunks: chunk => {
					const name = chunk.name;
					if (name) {
						// this setup only applies to a subset of entry points, not to everything defined in config.entry
						return name.startsWith('site/');
					}

					// TODO: no name - is it on-demand chunks? not sure. in the meantime, exclude them from this common chunks setup
					return false;
				},
				enforce: true,
				// only extract modules that are also reused by other chunks!
				minChunks: 2,
				priority: -20,
			},
		}
	}
},

The workaround is to get rid of vendor-entry entry chunk, and import it from your real entry point(s):

// main.js
import 'path/to/vendor-entry'; // this will execute all vendor-specific bootstrap code

console.log('main.js entry point')
// rest of main.js logic

// webpack.config
entries: {
  main: 'path/to/main'
},
cacheGroups: {
  vendor: {
    test: // only include vendor modules: using a regexp or a function
    minChunks: 1, // extract a vendor module into the 'vendor' chunk even if it's only used in a single entry chunk (main.js)
  }
}

@amakhrov if you got it working, do you mind sharing your config? I am still struggling to convert to webpack 4 because of this issue.

@snapwich DllPlugin /DllReferencePlugin provided by webpack should give you more control over code splitting. You could use them with ExecutableDllPlugin to execute particular entry points within the resulted bundle.

You could use DllPlugin and DllReferencePlugin for code splitting and ExecutableDllPlugin to execute a bundle with shared modules.

Finally, i could get a single vendors bundle that contains all vendor code while keeping the code of top-level entries in their own bundle. Also, my library(ProjectLib) is in its own bundle instead of vendors.

- main.ts
import "vendors.ts";
... some common code for application

- vendors.ts
import "jquery";
import "bootstrap";
import "datatables.net";

- page1.ts
import "ProjectLib/MyPlugin.ts"

- page2.ts
import "ProjectLib/MyCommon.ts"

- webpack.dev.js
const ProjectLibDir = path.resolve(__dirname, "ProjectLib");
entry: {
    main: path.resolve(__dirname, "main.ts"),
    page1: path.resolve(__dirname, "page1.ts"),
    page2: path.resolve(__dirname, "page2.ts"),
},
optimization: {
    splitChunks: {
        cacheGroups: {
            projectLib: {
                test: (module, chunks) => module.context == ProjectLibDir,
                name: "projectLib",
                chunks: "all",
                minChunks: 1,
                minSize: 0,
                priority: 20,
                enforce: true
            },
            vendors: {
                test: (module, chunks) => module.depth > 0,
                name: "vendors",
                chunks: "all",
                minChunks: 1,
                minSize: 0,
                priority: 10,
                enforce: true
            }
        }
    },
    runtimeChunk: { name: "manifest" },
},

It depends on how you define ‘vendor’ modules.

See the test function in my example above. The function takes the name of the module and the array of chunks which have this module included.

If you want to extract all modules used by your vendor-entry file, you would do smth like

test: (module, chunks) => {
  const names = chunks
    .map(c => c.name)
    .filter(Boolean);

  return names.some(name => name === 'path/vendor-entry');
}

@expressiveco your config looks ok to me. It should create a vendor.js chunk (in addition to the vendor-entry.js and main.js entry chunks), containing modules that are shared across the both entry chunks.

What doesn’t go as expected here for you?

I came across the same issue. In my old webpack config the ‘common’ chunk served both as a chunk with shared modules and an entry point. In webpack 4 it no longer works.

As a workaround I ended up having two chunks: ‘common’ and ‘common-entry’. They are always included on all pages together - so there is no good reason for them not to be a single file.