Skip to content

Commit

Permalink
feature #680 Add Encore.addCacheGroup() method and depreciate Encore.…
Browse files Browse the repository at this point in the history
…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()
  • Loading branch information
weaverryan committed Mar 27, 2020
2 parents 4ce6f35 + 7e32b0b commit e773dda
Show file tree
Hide file tree
Showing 13 changed files with 569 additions and 3 deletions.
9 changes: 9 additions & 0 deletions fixtures/preact/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { h, Component } from 'preact';

export default class App extends Component {
render() {
return (
<h1>This is a React component!</h1>
);
}
}
5 changes: 5 additions & 0 deletions fixtures/preact/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { h, render } from 'preact';

import App from './App';

render(<App />, document.getElementById('app'));
47 changes: 47 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,53 @@ class Encore {
return this;
}

/**
* Add a new cache group to Webpack's SplitChunksPlugin.
* This can, for instance, be used to extract code that
* is common to multiple entries into its own chunk.
*
* See: https://webpack.js.org/plugins/split-chunks-plugin/#examples
*
* For example:
*
* ```
* Encore.addCacheGroup('vendor', {
* test: /[\\/]node_modules[\\/]react/
* });
* ```
*
* You can pass all the options supported by the SplitChunksPlugin
* but also the following shorthand provided by Encore:
*
* * `node_modules`: An array of `node_modules` packages names
*
* For example:
*
* ```
* Encore.addCacheGroup('vendor', {
* node_modules: ['react', 'react-dom']
* });
* ```
*
* At least one of the `test` or the `node_modules` option
* should be provided.
*
* By default, the new cache group will be created with the
* following options:
* * `chunks` set to `"all"`
* * `enforce` set to `true`
* * `name` set to the value of the "name" parameter
*
* @param {string} name The chunk name (e.g. vendor to create a vendor.js)
* @param {object} options Cache group option
* @returns {Encore}
*/
addCacheGroup(name, options) {
webpackConfig.addCacheGroup(name, options);

return this;
}

/**
* Copy files or folders to the build directory.
*
Expand Down
34 changes: 34 additions & 0 deletions lib/WebpackConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const fs = require('fs');
const crypto = require('crypto');
const RuntimeConfig = require('./config/RuntimeConfig'); //eslint-disable-line no-unused-vars
const logger = require('./logger');
const regexpEscaper = require('./utils/regexp-escaper');

/**
* @param {RuntimeConfig|null} runtimeConfig
Expand Down Expand Up @@ -84,6 +85,7 @@ class WebpackConfig {
this.manifestKeyPrefix = null;
this.sharedCommonsEntryName = null;
this.sharedCommonsEntryFile = null;
this.cacheGroups = {};
this.providedVariables = {};
this.configuredFilenames = {};
this.aliases = {};
Expand Down Expand Up @@ -511,6 +513,8 @@ class WebpackConfig {
}

createSharedEntry(name, file) {
logger.deprecation('Encore.createSharedEntry() is deprecated and will be removed in a future version, please use Encore.splitEntryChunks() or Encore.addCacheGroup() instead.');

if (this.shouldSplitEntryChunks) {
throw new Error('Using splitEntryChunks() and createSharedEntry() together is not supported. Use one of these strategies only to optimize your build.');
}
Expand All @@ -530,6 +534,36 @@ class WebpackConfig {
this.addEntry(name, file);
}

addCacheGroup(name, options) {
if (typeof name !== 'string') {
throw new Error('Argument 1 to addCacheGroup() must be a string.');
}

if (typeof options !== 'object') {
throw new Error('Argument 2 to addCacheGroup() must be an object.');
}

if (!options['test'] && !options['node_modules']) {
throw new Error('Either the "test" option or the "node_modules" option of addCacheGroup() must be set');
}

if (options['node_modules']) {
if (!Array.isArray(options['node_modules'])) {
throw new Error('The "node_modules" option of addCacheGroup() must be an array');
}

options.test = new RegExp(`[\\\\/]node_modules[\\\\/](${
options['node_modules']
.map(regexpEscaper)
.join('|')
})[\\\\/]`);

delete options['node_modules'];
}

this.cacheGroups[name] = options;
}

copyFiles(configs = []) {
if (!Array.isArray(configs)) {
configs = [configs];
Expand Down
18 changes: 15 additions & 3 deletions lib/config-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -514,8 +514,20 @@ class ConfigGenerator {
splitChunks.name = false;
}

const cacheGroups = {};

for (const groupName in this.webpackConfig.cacheGroups) {
cacheGroups[groupName] = Object.assign(
{
name: groupName,
chunks: 'all',
enforce: true
},
this.webpackConfig.cacheGroups[groupName]
);
}

if (this.webpackConfig.sharedCommonsEntryName) {
const cacheGroups = {};
cacheGroups[this.webpackConfig.sharedCommonsEntryName] = {
chunks: 'initial',
name: this.webpackConfig.sharedCommonsEntryName,
Expand All @@ -527,10 +539,10 @@ class ConfigGenerator {
// *definitely* included.
enforce: true,
};

splitChunks.cacheGroups = cacheGroups;
}

splitChunks.cacheGroups = cacheGroups;

switch (this.webpackConfig.shouldUseSingleRuntimeChunk) {
case true:
// causes a runtime.js to be emitted with the Webpack runtime
Expand Down
14 changes: 14 additions & 0 deletions lib/config/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ class Validator {
this._validateDevServer();

this._validateSharedEntryName();

this._validateCacheGroupNames();
}

_validateBasic() {
Expand Down Expand Up @@ -75,6 +77,18 @@ class Validator {
logger.warning(`Passing "${this.webpackConfig.sharedCommonsEntryName}" to createSharedEntry() is not recommended, as it will override the built-in cache group by this name.`);
}
}

_validateCacheGroupNames() {
for (const groupName of Object.keys(this.webpackConfig.cacheGroups)) {
if (['vendors', 'defaultVendors', 'default'].includes(groupName)) {
logger.warning(`Passing "${groupName}" to addCacheGroup() is not recommended, as it will override the built-in cache group by this name.`);
}

if (groupName === this.webpackConfig.sharedCommonsEntryName) {
logger.warning('Using the same name when calling createSharedEntry() and addCacheGroup() is not recommended.');
}
}
}
}

module.exports = function(webpackConfig) {
Expand Down
22 changes: 22 additions & 0 deletions lib/utils/regexp-escaper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

'use strict';

/**
* Function that escapes a string so it can be used in a RegExp.
*
* See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping
*
* @param {string} str
* @return {string}
*/
module.exports = function regexpEscaper(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
};
71 changes: 71 additions & 0 deletions test/WebpackConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,77 @@ describe('WebpackConfig object', () => {
});
});

describe('addCacheGroup', () => {
it('Calling it adds cache groups', () => {
const config = createConfig();
config.addCacheGroup('foo', { test: /foo/ });
config.addCacheGroup('bar', { test: /bar/ });

expect(config.cacheGroups).to.deep.equal({
foo: { test: /foo/ },
bar: { test: /bar/ },
});
});

it('Calling it using the "node_modules" option', () => {
const config = createConfig();
config.addCacheGroup('foo', { node_modules: ['foo','bar', 'baz'] });

expect(config.cacheGroups).to.deep.equal({
foo: {
test: /[\\/]node_modules[\\/](foo|bar|baz)[\\/]/,
},
});
});

it('Calling it with other SplitChunksPlugin options', () => {
const config = createConfig();
config.addCacheGroup('foo', {
test: /foo/,
chunks: 'initial',
minChunks: 2
});

expect(config.cacheGroups).to.deep.equal({
foo: {
test: /foo/,
chunks: 'initial',
minChunks: 2
},
});
});

it('Calling it with an invalid name', () => {
const config = createConfig();
expect(() => {
config.addCacheGroup(true, { test: /foo/ });
}).to.throw('must be a string');
});

it('Calling it with an invalid options parameter', () => {
const config = createConfig();
expect(() => {
config.addCacheGroup('foo', 'bar');
}).to.throw('must be an object');
});

it('Calling it with an invalid node_modules option', () => {
const config = createConfig();
expect(() => {
config.addCacheGroup('foo', {
'node_modules': 'foo'
});
}).to.throw('must be an array');
});

it('Calling it without the "test" or "node_modules" option', () => {
const config = createConfig();
expect(() => {
config.addCacheGroup('foo', { type: 'json' });
}).to.throw('Either the "test" option or the "node_modules" option');
});
});

describe('copyFiles', () => {
it('Calling it adds files to be copied', () => {
const config = createConfig();
Expand Down
66 changes: 66 additions & 0 deletions test/config-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -1362,4 +1362,70 @@ describe('The config-generator function', () => {
}).to.not.throw();
});
});

describe('Test addCacheGroup()', () => {
it('Calling it adds cache groups', () => {
const config = createConfig();
config.outputPath = '/tmp/output/public-path';
config.publicPath = '/public-path';
config.enableSingleRuntimeChunk();
config.addEntry('main', './main');
config.addCacheGroup('foo', { test: /foo/ });
config.addCacheGroup('bar', { test: /bar/ });

const actualConfig = configGenerator(config);

expect(actualConfig.optimization.splitChunks.cacheGroups).to.deep.equal({
foo: { name: 'foo', test: /foo/, chunks: 'all', enforce: true },
bar: { name: 'bar', test: /bar/, chunks: 'all', enforce: true },
});
});

it('Calling it using the "node_modules" option', () => {
const config = createConfig();
config.outputPath = '/tmp/output/public-path';
config.publicPath = '/public-path';
config.enableSingleRuntimeChunk();
config.addEntry('main', './main');
config.addCacheGroup('foo', { node_modules: ['foo','bar', 'baz'] });

const actualConfig = configGenerator(config);

expect(actualConfig.optimization.splitChunks.cacheGroups).to.deep.equal({
foo: {
name: 'foo',
test: /[\\/]node_modules[\\/](foo|bar|baz)[\\/]/,
chunks: 'all',
enforce: true,
},
});
});

it('Calling it and overriding default options', () => {
const config = createConfig();
config.outputPath = '/tmp/output/public-path';
config.publicPath = '/public-path';
config.enableSingleRuntimeChunk();
config.addEntry('main', './main');
config.addCacheGroup('foo', {
name: 'bar',
test: /foo/,
chunks: 'initial',
minChunks: 2,
enforce: false,
});

const actualConfig = configGenerator(config);

expect(actualConfig.optimization.splitChunks.cacheGroups).to.deep.equal({
foo: {
name: 'bar',
test: /foo/,
chunks: 'initial',
minChunks: 2,
enforce: false,
},
});
});
});
});

0 comments on commit e773dda

Please sign in to comment.