Skip to content

Commit cec8358

Browse files
committed
bug #462 Always used named chunks AND prevent split chunks from changing too often. (weaverryan)
This PR was squashed before being merged into the master branch (closes #462). Discussion ---------- Always used named chunks AND prevent split chunks from changing too often. Fixes #461 But, this does not expose the entry names internally in the code (as these are used instead of auto-incremet ids). I don't think there's a way around that. Also, I'm still tracking down a somewhat related but that causes a related issue (meaningless id's changing in generated code) under very specific conditions. In fact, you can see it in this test case just by changing all the 3 of the entries to an array - e.g. ```js config.addEntry('main1', ['./js/code_splitting']); if (includeExtraEntry) { config.addEntry('main2', ['./js/eslint']); } config.addEntry('main3', ['./js/no_require']); ``` That doesn't need to block this PR, but I need to figure out the issue (it's Webpack behavior) and see if it affects versioning (i.e. if the contents change but the file hashes do NOT change, this is a BIG problem, otherwise, it's more of a minor, performance problem as filenames would just be changing too often). Commits ------- a8b6725 Making how the splitting works more consistent 4de5536 Fixing tests 13e07c6 linking to issue 7c96476 Fixing a bug where split filenames might change too often a426b8c Always used named chunks to avoid caching problems
2 parents 9a4030c + a8b6725 commit cec8358

File tree

3 files changed

+141
-36
lines changed

3 files changed

+141
-36
lines changed

lib/config-generator.js

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -403,27 +403,22 @@ class ConfigGenerator {
403403
// which can be useful for debugging, especially with HMR
404404
optimization.namedModules = true;
405405
}
406+
// https://github.com/webpack/webpack/issues/8354
407+
// likely can be removed in Webpack 5
408+
// https://github.com/webpack/webpack/pull/8374
409+
optimization.chunkIds = 'named';
406410

407411
let splitChunks = {
408412
chunks: this.webpackConfig.shouldSplitEntryChunks ? 'all' : 'async'
409413
};
410414

411-
// hash the split filenames in production to protect exposing entry names
415+
// This causes the split filenames (& internal names) to be,
416+
// for example, 0.js. This is needed so that the filename
417+
// doesn't change suddenly when another entry needs that same
418+
// shared code (e.g. vendor~entry1~entry2.js).
419+
// https://github.com/webpack/webpack/issues/8426#issuecomment-442375207
412420
if (this.webpackConfig.shouldSplitEntryChunks && this.webpackConfig.isProduction()) {
413-
// taken from SplitChunksPlugin
414-
const hashFilename = name => {
415-
return crypto
416-
.createHash('md4')
417-
.update(name)
418-
.digest('hex')
419-
.slice(0, 8);
420-
};
421-
422-
splitChunks.name = function(module, chunks, cacheGroup) {
423-
const names = chunks.map(c => c.name);
424-
425-
return cacheGroup + '~' + hashFilename(names.join('~'));
426-
};
421+
splitChunks.name = false;
427422
}
428423

429424
if (this.webpackConfig.sharedCommonsEntryName) {

test/config-generator.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -877,7 +877,7 @@ describe('The config-generator function', () => {
877877

878878
const actualConfig = configGenerator(config);
879879
expect(actualConfig.optimization.splitChunks.chunks).to.equal('all');
880-
expect(actualConfig.optimization.splitChunks.name).to.be.a('function');
880+
expect(actualConfig.optimization.splitChunks.name).to.be.false;
881881
});
882882
});
883883

test/functional.js

Lines changed: 130 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,7 @@ function createWebpackConfig(outputDirName = '', command, argv = {}) {
3131
}
3232

3333
function convertToManifestPath(assetSrc, webpackConfig) {
34-
const manifestData = JSON.parse(
35-
fs.readFileSync(path.join(webpackConfig.outputPath, 'manifest.json'), 'utf8')
36-
);
34+
const manifestData = JSON.parse(readOutputFileContents('manifest.json', webpackConfig));
3735

3836
if (typeof manifestData[assetSrc] === 'undefined') {
3937
throw new Error(`Path ${assetSrc} not found in manifest!`);
@@ -42,6 +40,26 @@ function convertToManifestPath(assetSrc, webpackConfig) {
4240
return manifestData[assetSrc];
4341
}
4442

43+
function readOutputFileContents(filename, config) {
44+
const fullPath = path.join(config.outputPath, filename);
45+
46+
if (!fs.existsSync(fullPath)) {
47+
throw new Error(`Output file "${filename}" does not exist.`);
48+
}
49+
50+
return fs.readFileSync(fullPath, 'utf8');
51+
}
52+
53+
function getEntrypointData(config, entryName) {
54+
const entrypointsData = JSON.parse(readOutputFileContents('entrypoints.json', config));
55+
56+
if (typeof entrypointsData.entrypoints[entryName] === 'undefined') {
57+
throw new Error(`The entry ${entryName} was not found!`);
58+
}
59+
60+
return entrypointsData.entrypoints[entryName];
61+
}
62+
4563
describe('Functional tests using webpack', function() {
4664
// being functional tests, these can take quite long
4765
this.timeout(8000);
@@ -318,6 +336,22 @@ describe('Functional tests using webpack', function() {
318336
});
319337
});
320338

339+
it('.mjs files are supported natively', (done) => {
340+
const config = createWebpackConfig('web/build', 'dev');
341+
config.addEntry('main', './js/hello_world');
342+
config.setPublicPath('/build');
343+
344+
testSetup.runWebpack(config, (webpackAssert) => {
345+
// check that main.js has the correct contents
346+
webpackAssert.assertOutputFileContains(
347+
'main.js',
348+
'Hello World!'
349+
);
350+
351+
done();
352+
});
353+
});
354+
321355
describe('addStyleEntry .js files are removed', () => {
322356
it('Without versioning', (done) => {
323357
const config = createWebpackConfig('web', 'dev');
@@ -1628,7 +1662,7 @@ module.exports = {
16281662
});
16291663
});
16301664

1631-
describe('entrypoints.json', () => {
1665+
describe('entrypoints.json & splitChunks()', () => {
16321666
it('Use "all" splitChunks & look at entrypoints.json', (done) => {
16331667
const config = createWebpackConfig('web/build', 'dev');
16341668
config.addEntry('main', ['./css/roboto_font.css', './js/no_require', 'vue']);
@@ -1760,18 +1794,18 @@ module.exports = {
17601794
webpackAssert.assertOutputJsonFileMatches('entrypoints.json', {
17611795
entrypoints: {
17621796
main: {
1763-
js: ['/build/runtime.js', '/build/vendors~cc515e6e.js', '/build/default~cc515e6e.js', '/build/main.js'],
1764-
css: ['/build/default~cc515e6e.css']
1797+
js: ['/build/runtime.js', '/build/0.js', '/build/1.js', '/build/main.js'],
1798+
css: ['/build/1.css']
17651799
},
17661800
other: {
1767-
js: ['/build/runtime.js', '/build/vendors~cc515e6e.js', '/build/default~cc515e6e.js', '/build/other.js'],
1768-
css: ['/build/default~cc515e6e.css']
1801+
js: ['/build/runtime.js', '/build/0.js', '/build/1.js', '/build/other.js'],
1802+
css: ['/build/1.css']
17691803
}
17701804
}
17711805
});
17721806

17731807
// make split chunks are correct in manifest
1774-
webpackAssert.assertManifestKeyExists('build/vendors~cc515e6e.js');
1808+
webpackAssert.assertManifestKeyExists('build/0.js');
17751809

17761810
done();
17771811
});
@@ -1809,19 +1843,95 @@ module.exports = {
18091843
});
18101844
});
18111845

1812-
it('.mjs files are supported natively', (done) => {
1813-
const config = createWebpackConfig('web/build', 'dev');
1814-
config.addEntry('main', './js/hello_world');
1815-
config.setPublicPath('/build');
1846+
it('Make sure chunkIds do not change between builds', (done) => {
1847+
// https://github.com/symfony/webpack-encore/issues/461
1848+
const createSimilarConfig = function(includeExtraEntry) {
1849+
const config = createWebpackConfig('web/build', 'production');
1850+
config.addEntry('main1', './js/code_splitting');
1851+
if (includeExtraEntry) {
1852+
config.addEntry('main2', './js/eslint');
1853+
}
1854+
config.addEntry('main3', './js/no_require');
1855+
config.setPublicPath('/build');
18161856

1817-
testSetup.runWebpack(config, (webpackAssert) => {
1818-
// check that main.js has the correct contents
1819-
webpackAssert.assertOutputFileContains(
1820-
'main.js',
1821-
'Hello World!'
1822-
);
1857+
return config;
1858+
};
18231859

1824-
done();
1860+
const configA = createSimilarConfig(false);
1861+
const configB = createSimilarConfig(true);
1862+
1863+
testSetup.runWebpack(configA, () => {
1864+
testSetup.runWebpack(configB, () => {
1865+
const main3Contents = readOutputFileContents('main3.js', configA);
1866+
const finalMain3Contents = readOutputFileContents('main3.js', configB);
1867+
1868+
if (finalMain3Contents !== main3Contents) {
1869+
throw new Error(`Contents after first compile do not match after second compile: \n\n ${main3Contents} \n\n versus \n\n ${finalMain3Contents} \n`);
1870+
}
1871+
1872+
done();
1873+
});
1874+
});
1875+
});
1876+
1877+
it('Do not change contents or filenames when more modules require the same split contents', (done) => {
1878+
const createSimilarConfig = function(includeExtraEntry) {
1879+
const config = createWebpackConfig('web/build', 'production');
1880+
config.addEntry('main1', ['./js/code_splitting', 'preact']);
1881+
config.addEntry('main3', ['./js/no_require', 'preact']);
1882+
if (includeExtraEntry) {
1883+
config.addEntry('main4', ['./js/eslint', 'preact']);
1884+
}
1885+
config.setPublicPath('/build');
1886+
config.splitEntryChunks();
1887+
config.configureSplitChunks((splitChunks) => {
1888+
// will include preact, but prevent any other splitting
1889+
splitChunks.minSize = 10000;
1890+
});
1891+
1892+
return config;
1893+
};
1894+
1895+
const getSplitVendorJsPath = function(config) {
1896+
const entrypointData = getEntrypointData(config, 'main3');
1897+
1898+
const splitFiles = entrypointData.js.filter(filename => {
1899+
return filename !== '/build/runtime.js' && filename !== '/build/main3.js';
1900+
});
1901+
1902+
// sanity check
1903+
if (splitFiles.length !== 1) {
1904+
throw new Error(`Unexpected number (${splitFiles.length}) of split files for main3 entry`);
1905+
}
1906+
1907+
return splitFiles[0];
1908+
};
1909+
1910+
const configA = createSimilarConfig(false);
1911+
const configB = createSimilarConfig(true);
1912+
1913+
testSetup.runWebpack(configA, () => {
1914+
testSetup.runWebpack(configB, () => {
1915+
const vendorPath = getSplitVendorJsPath(configA);
1916+
const finalVendorPath = getSplitVendorJsPath(configB);
1917+
1918+
// make sure that the filename of the split vendor file didn't change,
1919+
// even though an additional entry is now sharing its contents
1920+
if (finalVendorPath !== vendorPath) {
1921+
throw new Error(`Vendor filename changed! Before ${vendorPath} and after ${finalVendorPath}.`);
1922+
}
1923+
1924+
// make sure that, internally, the split chunk name did not change,
1925+
// which would cause the contents of main3 to suddenly change
1926+
const main3Contents = readOutputFileContents('main3.js', configA);
1927+
const finalMain3Contents = readOutputFileContents('main3.js', configB);
1928+
1929+
if (finalMain3Contents !== main3Contents) {
1930+
throw new Error(`Contents after first compile do not match after second compile: \n\n ${main3Contents} \n\n versus \n\n ${finalMain3Contents} \n`);
1931+
}
1932+
1933+
done();
1934+
});
18251935
});
18261936
});
18271937
});

0 commit comments

Comments
 (0)