Skip to content

Commit e773dda

Browse files
committed
feature #680 Add Encore.addCacheGroup() method and depreciate Encore.createSharedEntry() (Lyrkan)
This PR was squashed before being merged into the master branch (closes #680). Discussion ---------- Add Encore.addCacheGroup() method and depreciate Encore.createSharedEntry() As discussed in #645 (comment) our `Encore.createSharedEntry()` hack won't work anymore with Webpack 5. Since it was mostly something that was added to make the transition from Webpack 3 to Webpack 4 less painful it's probably time to depreciate it and encourage people to use cache groups properly instead. This PR adds a new `Encore.addCacheGroup()` method that, as its name implies, simply adds a new cache group to the config: ```js Encore.addCacheGroup('vendor', { test: /[\\/]node_modules[\\/]react/ }); ``` To make it a bit easier in case people want to directly reference things from the `node_modules` directory I also added a `node_modules` option which is basically a shorthand that sets the `test` option: ```js Encore.addCacheGroup('vendor', { node_modules: ['react', 'react-dom'] }); ``` Commits ------- 7e32b0b Add a warning when calling addCacheGroup() with 'vendors' or the same than in createSharedEntry() e4095b3 Add Encore.addCacheGroup() method and depreciate Encore.createSharedEntry()
2 parents 4ce6f35 + 7e32b0b commit e773dda

13 files changed

+569
-3
lines changed

fixtures/preact/App.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { h, Component } from 'preact';
2+
3+
export default class App extends Component {
4+
render() {
5+
return (
6+
<h1>This is a React component!</h1>
7+
);
8+
}
9+
}

fixtures/preact/main.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { h, render } from 'preact';
2+
3+
import App from './App';
4+
5+
render(<App />, document.getElementById('app'));

index.js

+47
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,53 @@ class Encore {
476476
return this;
477477
}
478478

479+
/**
480+
* Add a new cache group to Webpack's SplitChunksPlugin.
481+
* This can, for instance, be used to extract code that
482+
* is common to multiple entries into its own chunk.
483+
*
484+
* See: https://webpack.js.org/plugins/split-chunks-plugin/#examples
485+
*
486+
* For example:
487+
*
488+
* ```
489+
* Encore.addCacheGroup('vendor', {
490+
* test: /[\\/]node_modules[\\/]react/
491+
* });
492+
* ```
493+
*
494+
* You can pass all the options supported by the SplitChunksPlugin
495+
* but also the following shorthand provided by Encore:
496+
*
497+
* * `node_modules`: An array of `node_modules` packages names
498+
*
499+
* For example:
500+
*
501+
* ```
502+
* Encore.addCacheGroup('vendor', {
503+
* node_modules: ['react', 'react-dom']
504+
* });
505+
* ```
506+
*
507+
* At least one of the `test` or the `node_modules` option
508+
* should be provided.
509+
*
510+
* By default, the new cache group will be created with the
511+
* following options:
512+
* * `chunks` set to `"all"`
513+
* * `enforce` set to `true`
514+
* * `name` set to the value of the "name" parameter
515+
*
516+
* @param {string} name The chunk name (e.g. vendor to create a vendor.js)
517+
* @param {object} options Cache group option
518+
* @returns {Encore}
519+
*/
520+
addCacheGroup(name, options) {
521+
webpackConfig.addCacheGroup(name, options);
522+
523+
return this;
524+
}
525+
479526
/**
480527
* Copy files or folders to the build directory.
481528
*

lib/WebpackConfig.js

+34
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const fs = require('fs');
1414
const crypto = require('crypto');
1515
const RuntimeConfig = require('./config/RuntimeConfig'); //eslint-disable-line no-unused-vars
1616
const logger = require('./logger');
17+
const regexpEscaper = require('./utils/regexp-escaper');
1718

1819
/**
1920
* @param {RuntimeConfig|null} runtimeConfig
@@ -84,6 +85,7 @@ class WebpackConfig {
8485
this.manifestKeyPrefix = null;
8586
this.sharedCommonsEntryName = null;
8687
this.sharedCommonsEntryFile = null;
88+
this.cacheGroups = {};
8789
this.providedVariables = {};
8890
this.configuredFilenames = {};
8991
this.aliases = {};
@@ -511,6 +513,8 @@ class WebpackConfig {
511513
}
512514

513515
createSharedEntry(name, file) {
516+
logger.deprecation('Encore.createSharedEntry() is deprecated and will be removed in a future version, please use Encore.splitEntryChunks() or Encore.addCacheGroup() instead.');
517+
514518
if (this.shouldSplitEntryChunks) {
515519
throw new Error('Using splitEntryChunks() and createSharedEntry() together is not supported. Use one of these strategies only to optimize your build.');
516520
}
@@ -530,6 +534,36 @@ class WebpackConfig {
530534
this.addEntry(name, file);
531535
}
532536

537+
addCacheGroup(name, options) {
538+
if (typeof name !== 'string') {
539+
throw new Error('Argument 1 to addCacheGroup() must be a string.');
540+
}
541+
542+
if (typeof options !== 'object') {
543+
throw new Error('Argument 2 to addCacheGroup() must be an object.');
544+
}
545+
546+
if (!options['test'] && !options['node_modules']) {
547+
throw new Error('Either the "test" option or the "node_modules" option of addCacheGroup() must be set');
548+
}
549+
550+
if (options['node_modules']) {
551+
if (!Array.isArray(options['node_modules'])) {
552+
throw new Error('The "node_modules" option of addCacheGroup() must be an array');
553+
}
554+
555+
options.test = new RegExp(`[\\\\/]node_modules[\\\\/](${
556+
options['node_modules']
557+
.map(regexpEscaper)
558+
.join('|')
559+
})[\\\\/]`);
560+
561+
delete options['node_modules'];
562+
}
563+
564+
this.cacheGroups[name] = options;
565+
}
566+
533567
copyFiles(configs = []) {
534568
if (!Array.isArray(configs)) {
535569
configs = [configs];

lib/config-generator.js

+15-3
Original file line numberDiff line numberDiff line change
@@ -514,8 +514,20 @@ class ConfigGenerator {
514514
splitChunks.name = false;
515515
}
516516

517+
const cacheGroups = {};
518+
519+
for (const groupName in this.webpackConfig.cacheGroups) {
520+
cacheGroups[groupName] = Object.assign(
521+
{
522+
name: groupName,
523+
chunks: 'all',
524+
enforce: true
525+
},
526+
this.webpackConfig.cacheGroups[groupName]
527+
);
528+
}
529+
517530
if (this.webpackConfig.sharedCommonsEntryName) {
518-
const cacheGroups = {};
519531
cacheGroups[this.webpackConfig.sharedCommonsEntryName] = {
520532
chunks: 'initial',
521533
name: this.webpackConfig.sharedCommonsEntryName,
@@ -527,10 +539,10 @@ class ConfigGenerator {
527539
// *definitely* included.
528540
enforce: true,
529541
};
530-
531-
splitChunks.cacheGroups = cacheGroups;
532542
}
533543

544+
splitChunks.cacheGroups = cacheGroups;
545+
534546
switch (this.webpackConfig.shouldUseSingleRuntimeChunk) {
535547
case true:
536548
// causes a runtime.js to be emitted with the Webpack runtime

lib/config/validator.js

+14
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ class Validator {
2929
this._validateDevServer();
3030

3131
this._validateSharedEntryName();
32+
33+
this._validateCacheGroupNames();
3234
}
3335

3436
_validateBasic() {
@@ -75,6 +77,18 @@ class Validator {
7577
logger.warning(`Passing "${this.webpackConfig.sharedCommonsEntryName}" to createSharedEntry() is not recommended, as it will override the built-in cache group by this name.`);
7678
}
7779
}
80+
81+
_validateCacheGroupNames() {
82+
for (const groupName of Object.keys(this.webpackConfig.cacheGroups)) {
83+
if (['vendors', 'defaultVendors', 'default'].includes(groupName)) {
84+
logger.warning(`Passing "${groupName}" to addCacheGroup() is not recommended, as it will override the built-in cache group by this name.`);
85+
}
86+
87+
if (groupName === this.webpackConfig.sharedCommonsEntryName) {
88+
logger.warning('Using the same name when calling createSharedEntry() and addCacheGroup() is not recommended.');
89+
}
90+
}
91+
}
7892
}
7993

8094
module.exports = function(webpackConfig) {

lib/utils/regexp-escaper.js

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* This file is part of the Symfony Webpack Encore package.
3+
*
4+
* (c) Fabien Potencier <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
'use strict';
11+
12+
/**
13+
* Function that escapes a string so it can be used in a RegExp.
14+
*
15+
* See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping
16+
*
17+
* @param {string} str
18+
* @return {string}
19+
*/
20+
module.exports = function regexpEscaper(str) {
21+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
22+
};

test/WebpackConfig.js

+71
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,77 @@ describe('WebpackConfig object', () => {
398398
});
399399
});
400400

401+
describe('addCacheGroup', () => {
402+
it('Calling it adds cache groups', () => {
403+
const config = createConfig();
404+
config.addCacheGroup('foo', { test: /foo/ });
405+
config.addCacheGroup('bar', { test: /bar/ });
406+
407+
expect(config.cacheGroups).to.deep.equal({
408+
foo: { test: /foo/ },
409+
bar: { test: /bar/ },
410+
});
411+
});
412+
413+
it('Calling it using the "node_modules" option', () => {
414+
const config = createConfig();
415+
config.addCacheGroup('foo', { node_modules: ['foo','bar', 'baz'] });
416+
417+
expect(config.cacheGroups).to.deep.equal({
418+
foo: {
419+
test: /[\\/]node_modules[\\/](foo|bar|baz)[\\/]/,
420+
},
421+
});
422+
});
423+
424+
it('Calling it with other SplitChunksPlugin options', () => {
425+
const config = createConfig();
426+
config.addCacheGroup('foo', {
427+
test: /foo/,
428+
chunks: 'initial',
429+
minChunks: 2
430+
});
431+
432+
expect(config.cacheGroups).to.deep.equal({
433+
foo: {
434+
test: /foo/,
435+
chunks: 'initial',
436+
minChunks: 2
437+
},
438+
});
439+
});
440+
441+
it('Calling it with an invalid name', () => {
442+
const config = createConfig();
443+
expect(() => {
444+
config.addCacheGroup(true, { test: /foo/ });
445+
}).to.throw('must be a string');
446+
});
447+
448+
it('Calling it with an invalid options parameter', () => {
449+
const config = createConfig();
450+
expect(() => {
451+
config.addCacheGroup('foo', 'bar');
452+
}).to.throw('must be an object');
453+
});
454+
455+
it('Calling it with an invalid node_modules option', () => {
456+
const config = createConfig();
457+
expect(() => {
458+
config.addCacheGroup('foo', {
459+
'node_modules': 'foo'
460+
});
461+
}).to.throw('must be an array');
462+
});
463+
464+
it('Calling it without the "test" or "node_modules" option', () => {
465+
const config = createConfig();
466+
expect(() => {
467+
config.addCacheGroup('foo', { type: 'json' });
468+
}).to.throw('Either the "test" option or the "node_modules" option');
469+
});
470+
});
471+
401472
describe('copyFiles', () => {
402473
it('Calling it adds files to be copied', () => {
403474
const config = createConfig();

test/config-generator.js

+66
Original file line numberDiff line numberDiff line change
@@ -1362,4 +1362,70 @@ describe('The config-generator function', () => {
13621362
}).to.not.throw();
13631363
});
13641364
});
1365+
1366+
describe('Test addCacheGroup()', () => {
1367+
it('Calling it adds cache groups', () => {
1368+
const config = createConfig();
1369+
config.outputPath = '/tmp/output/public-path';
1370+
config.publicPath = '/public-path';
1371+
config.enableSingleRuntimeChunk();
1372+
config.addEntry('main', './main');
1373+
config.addCacheGroup('foo', { test: /foo/ });
1374+
config.addCacheGroup('bar', { test: /bar/ });
1375+
1376+
const actualConfig = configGenerator(config);
1377+
1378+
expect(actualConfig.optimization.splitChunks.cacheGroups).to.deep.equal({
1379+
foo: { name: 'foo', test: /foo/, chunks: 'all', enforce: true },
1380+
bar: { name: 'bar', test: /bar/, chunks: 'all', enforce: true },
1381+
});
1382+
});
1383+
1384+
it('Calling it using the "node_modules" option', () => {
1385+
const config = createConfig();
1386+
config.outputPath = '/tmp/output/public-path';
1387+
config.publicPath = '/public-path';
1388+
config.enableSingleRuntimeChunk();
1389+
config.addEntry('main', './main');
1390+
config.addCacheGroup('foo', { node_modules: ['foo','bar', 'baz'] });
1391+
1392+
const actualConfig = configGenerator(config);
1393+
1394+
expect(actualConfig.optimization.splitChunks.cacheGroups).to.deep.equal({
1395+
foo: {
1396+
name: 'foo',
1397+
test: /[\\/]node_modules[\\/](foo|bar|baz)[\\/]/,
1398+
chunks: 'all',
1399+
enforce: true,
1400+
},
1401+
});
1402+
});
1403+
1404+
it('Calling it and overriding default options', () => {
1405+
const config = createConfig();
1406+
config.outputPath = '/tmp/output/public-path';
1407+
config.publicPath = '/public-path';
1408+
config.enableSingleRuntimeChunk();
1409+
config.addEntry('main', './main');
1410+
config.addCacheGroup('foo', {
1411+
name: 'bar',
1412+
test: /foo/,
1413+
chunks: 'initial',
1414+
minChunks: 2,
1415+
enforce: false,
1416+
});
1417+
1418+
const actualConfig = configGenerator(config);
1419+
1420+
expect(actualConfig.optimization.splitChunks.cacheGroups).to.deep.equal({
1421+
foo: {
1422+
name: 'bar',
1423+
test: /foo/,
1424+
chunks: 'initial',
1425+
minChunks: 2,
1426+
enforce: false,
1427+
},
1428+
});
1429+
});
1430+
});
13651431
});

0 commit comments

Comments
 (0)