Skip to content

Commit e4095b3

Browse files
committed
Add Encore.addCacheGroup() method and depreciate Encore.createSharedEntry()
1 parent b09eb03 commit e4095b3

13 files changed

+547
-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 = {};
@@ -509,6 +511,8 @@ class WebpackConfig {
509511
}
510512

511513
createSharedEntry(name, file) {
514+
logger.deprecation('Encore.createSharedEntry() is deprecated and will be removed in a future version, please use Encore.splitEntryChunks() or Encore.addCacheGroup() instead.');
515+
512516
if (this.shouldSplitEntryChunks) {
513517
throw new Error('Using splitEntryChunks() and createSharedEntry() together is not supported. Use one of these strategies only to optimize your build.');
514518
}
@@ -528,6 +532,36 @@ class WebpackConfig {
528532
this.addEntry(name, file);
529533
}
530534

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

lib/config-generator.js

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

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

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

lib/config/validator.js

+10
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,14 @@ 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 (['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+
}
7888
}
7989

8090
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
});

test/config/validator.js

+17
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,21 @@ describe('The validator function', () => {
9696
expect(logger.getMessages().warning).to.have.lengthOf(1);
9797
expect(logger.getMessages().warning[0]).to.include('Passing "vendors" to createSharedEntry() is not recommended');
9898
});
99+
100+
it('warning with addCacheGroup() and core cache group name', () => {
101+
const config = createConfig();
102+
config.outputPath = '/tmp/public/build';
103+
config.setPublicPath('/build');
104+
config.addEntry('main', './main');
105+
config.addCacheGroup('defaultVendors', {
106+
test: /[\\/]main/,
107+
});
108+
109+
logger.reset();
110+
logger.quiet();
111+
validator(config);
112+
113+
expect(logger.getMessages().warning).to.have.lengthOf(1);
114+
expect(logger.getMessages().warning[0]).to.include('Passing "defaultVendors" to addCacheGroup() is not recommended');
115+
});
99116
});

0 commit comments

Comments
 (0)