Skip to content

Add Encore.addCacheGroup() method and depreciate Encore.createSharedEntry() #680

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -509,6 +511,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 @@ -528,6 +532,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('|')
})[\\\\/]`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow :)


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 @@ -515,8 +515,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 @@ -528,10 +540,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,
},
});
});
});
});
Loading