Skip to content

Commit 58dd191

Browse files
committed
Add Encore.addCacheGroup() method and depreciate Encore.createSharedEntry()
1 parent a4ae7ce commit 58dd191

File tree

13 files changed

+547
-3
lines changed

13 files changed

+547
-3
lines changed

fixtures/preact/App.js

Lines changed: 9 additions & 0 deletions
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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 47 additions & 0 deletions
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

Lines changed: 34 additions & 0 deletions
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 = {};
@@ -500,6 +502,8 @@ class WebpackConfig {
500502
}
501503

502504
createSharedEntry(name, file) {
505+
logger.deprecation('Encore.createSharedEntry() is deprecated and will be removed in a future version, please use Encore.splitEntryChunks() or Encore.addCacheGroup() instead.');
506+
503507
if (this.shouldSplitEntryChunks) {
504508
throw new Error('Using splitEntryChunks() and createSharedEntry() together is not supported. Use one of these strategies only to optimize your build.');
505509
}
@@ -519,6 +523,36 @@ class WebpackConfig {
519523
this.addEntry(name, file);
520524
}
521525

526+
addCacheGroup(name, options) {
527+
if (typeof name !== 'string') {
528+
throw new Error('Argument 1 to addCacheGroup() must be a string.');
529+
}
530+
531+
if (typeof options !== 'object') {
532+
throw new Error('Argument 2 to addCacheGroup() must be an object.');
533+
}
534+
535+
if (!options['test'] && !options['node_modules']) {
536+
throw new Error('Either the "test" option or the "node_modules" option of addCacheGroup() must be set');
537+
}
538+
539+
if (options['node_modules']) {
540+
if (!Array.isArray(options['node_modules'])) {
541+
throw new Error('The "node_modules" option of addCacheGroup() must be an array');
542+
}
543+
544+
options.test = new RegExp(`[\\\\/]node_modules[\\\\/](${
545+
options['node_modules']
546+
.map(regexpEscaper)
547+
.join('|')
548+
})[\\\\/]`);
549+
550+
delete options['node_modules'];
551+
}
552+
553+
this.cacheGroups[name] = options;
554+
}
555+
522556
copyFiles(configs = []) {
523557
if (!Array.isArray(configs)) {
524558
configs = [configs];

lib/config-generator.js

Lines changed: 15 additions & 3 deletions
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

Lines changed: 10 additions & 0 deletions
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

Lines changed: 22 additions & 0 deletions
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 <fabien@symfony.com>
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

Lines changed: 71 additions & 0 deletions
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

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1303,4 +1303,70 @@ describe('The config-generator function', () => {
13031303
}).to.not.throw();
13041304
});
13051305
});
1306+
1307+
describe('Test addCacheGroup()', () => {
1308+
it('Calling it adds cache groups', () => {
1309+
const config = createConfig();
1310+
config.outputPath = '/tmp/output/public-path';
1311+
config.publicPath = '/public-path';
1312+
config.enableSingleRuntimeChunk();
1313+
config.addEntry('main', './main');
1314+
config.addCacheGroup('foo', { test: /foo/ });
1315+
config.addCacheGroup('bar', { test: /bar/ });
1316+
1317+
const actualConfig = configGenerator(config);
1318+
1319+
expect(actualConfig.optimization.splitChunks.cacheGroups).to.deep.equal({
1320+
foo: { name: 'foo', test: /foo/, chunks: 'all', enforce: true },
1321+
bar: { name: 'bar', test: /bar/, chunks: 'all', enforce: true },
1322+
});
1323+
});
1324+
1325+
it('Calling it using the "node_modules" option', () => {
1326+
const config = createConfig();
1327+
config.outputPath = '/tmp/output/public-path';
1328+
config.publicPath = '/public-path';
1329+
config.enableSingleRuntimeChunk();
1330+
config.addEntry('main', './main');
1331+
config.addCacheGroup('foo', { node_modules: ['foo','bar', 'baz'] });
1332+
1333+
const actualConfig = configGenerator(config);
1334+
1335+
expect(actualConfig.optimization.splitChunks.cacheGroups).to.deep.equal({
1336+
foo: {
1337+
name: 'foo',
1338+
test: /[\\/]node_modules[\\/](foo|bar|baz)[\\/]/,
1339+
chunks: 'all',
1340+
enforce: true,
1341+
},
1342+
});
1343+
});
1344+
1345+
it('Calling it and overriding default options', () => {
1346+
const config = createConfig();
1347+
config.outputPath = '/tmp/output/public-path';
1348+
config.publicPath = '/public-path';
1349+
config.enableSingleRuntimeChunk();
1350+
config.addEntry('main', './main');
1351+
config.addCacheGroup('foo', {
1352+
name: 'bar',
1353+
test: /foo/,
1354+
chunks: 'initial',
1355+
minChunks: 2,
1356+
enforce: false,
1357+
});
1358+
1359+
const actualConfig = configGenerator(config);
1360+
1361+
expect(actualConfig.optimization.splitChunks.cacheGroups).to.deep.equal({
1362+
foo: {
1363+
name: 'bar',
1364+
test: /foo/,
1365+
chunks: 'initial',
1366+
minChunks: 2,
1367+
enforce: false,
1368+
},
1369+
});
1370+
});
1371+
});
13061372
});

test/config/validator.js

Lines changed: 17 additions & 0 deletions
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 "vendors" to addCacheGroup() is not recommended');
115+
});
99116
});

0 commit comments

Comments
 (0)