Skip to content
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

[ST-5243] Make splitChunks configurable #167

Merged
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
1 change: 1 addition & 0 deletions packages/react-scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ npm start
- `babelIncludePrefixes`: An array of module name prefixes to opt into babel compilation, including local import module, e.g. `"../common"`. Includes `["@skyscanner/bpk-", "bpk-", "saddlebag-"]` by default.
- `enableAutomaticChunking`: Boolean, opt in to automatic chunking of vendor, common and app code.
- `vendorsChunkRegex`: String, Regex for picking what goes into the `vendors` chunk. See `cacheGroups` in webpack docs. Dependent on `enableAutomaticChunking` being enabled
- `splitChunksConfig`: Object, mapping to the [structure in the webpack docs](https://webpack.js.org/plugins/split-chunks-plugin/#optimizationsplitchunks). Applied only if `enableAutomaticChunking` is false, ignores `vendorsChunkRegex` if defined.
- `amdExcludes`: Array of module names to exclude from AMD parsing. Incldues `["lodash"]` by default.
- `externals`: exposing the Webpack config to modify externals, see [docs](https://webpack.js.org/configuration/externals/).
- `ssrExternals`: Similar to above, but for `ssr.js` only.
Expand Down
1 change: 1 addition & 0 deletions packages/react-scripts/backpack-addons/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Our react scripts fork includes a number of custom configuration items in order
| **ssrExternals** | The same as above `externals` except used for server side rendering only in **ssr.js** | **{}** |
| **enableAutomaticChunking** | Opts into automatic chunking of vender, common and app code.<br> When enabled the **splitChunks** plugin creates vender and common chunks which are split and when provided uses the `venderChunkRegex` to specify what is in each chunk.<br> When enabled **runtimeChunk** plugin creates a separate runtime chunk for projects to enable long term caching. | **false** |
| **vendorsChunkRegex** | Regex for picking what goes into the vendors chunk. Requires enableAutomaticChunking to be enabled.<br> See [cacheGroups](https://webpack.js.org/plugins/split-chunks-plugin/#splitchunkscachegroups) docs for further details. | |
| **splitChunksConfig** | Object, mapping to the [structure in the webpack docs](https://webpack.js.org/plugins/split-chunks-plugin/#optimizationsplitchunks).<br> Applied only if `enableAutomaticChunking` is false, ignores `vendorsChunkRegex` if defined. | |
| **sassFunctions** | This function encodes svg content into `base64` when there is a `bpk-icon` in the.scss file. | |

## How to add new feature
Expand Down
99 changes: 83 additions & 16 deletions packages/react-scripts/backpack-addons/splitChunks.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,93 @@
/**
* Defines a webpack splitChunks configuration, optionally based on consumer configuration.
*
* For automatic configuration set enableAutomaticChunking and optionally provide a vendorsChunkRegex string, e.g:
*
* // package.json
* ...
* "backpack-react-scripts": {
* ...
* "enableAutomaticChunking": true,
* "vendorsChunkRegex": "...",
* ...
* }
* ...
*
* For custom configuration disable enableAutomaticChunking and provide a configuration object, e.g:
*
* // package.json
* ...
* "backpack-react-scripts": {
* ...
* "enableAutomaticChunking": false,
* "splitChunksConfig": {
* "chunks": "all",
* ...
* "cacheGroups": {
* "vendors": {
* "test": "..."
* },
* "customChunk": {
* "test": "..."
* "priority": 100,
* "chunks": "all",
* "name": "customChunk",
* },
* },
* ...
* }
* ...
*
* References:
* https://webpack.js.org/plugins/split-chunks-plugin/#optimizationsplitchunks
* https://webpack.js.org/plugins/split-chunks-plugin/#splitchunkscachegroups
* https://twitter.com/wSokra/status/969633336732905474
* https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
*/

'use strict';

const paths = require('../config/paths');
const appPackageJson = require(paths.appPackageJson);
const bpkReactScriptsConfig = appPackageJson['backpack-react-scripts'] || {};

module.exports = (isEnvDevelopment) => {
// Automatically split vendor and commons
// https://twitter.com/wSokra/status/969633336732905474
// https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
return {
splitChunks: bpkReactScriptsConfig.enableAutomaticChunking
? {
module.exports = isEnvDevelopment => {
let splitChunksConfig = {};

// If opted in to automatic chunking, apply default configuration
if (bpkReactScriptsConfig.enableAutomaticChunking) {
splitChunksConfig = {
chunks: 'all',
name: isEnvDevelopment,
cacheGroups: bpkReactScriptsConfig.vendorsChunkRegex
? {
vendors: {
test: new RegExp(bpkReactScriptsConfig.vendorsChunkRegex)
},
}
: {},
cacheGroups: {},
};
// Apply vendorsChunkRegex if provided
if (bpkReactScriptsConfig.vendorsChunkRegex) {
splitChunksConfig.cacheGroups = {
vendors: {
// Regexes are passed as strings in package.json config, but need constructed here.
test: new RegExp(bpkReactScriptsConfig.vendorsChunkRegex),
},
};
}
}
// If not opted in to automatic chunking, use custom configuration - if defined.
else if (bpkReactScriptsConfig.splitChunksConfig) {
splitChunksConfig = {
...bpkReactScriptsConfig.splitChunksConfig,
name: isEnvDevelopment,
};
if (splitChunksConfig.cacheGroups) {
// Regexes are passed as strings in package.json config, but need constructed here.
for (let cacheGroup of Object.keys(splitChunksConfig.cacheGroups)) {
splitChunksConfig.cacheGroups[cacheGroup].test = new RegExp(
splitChunksConfig.cacheGroups[cacheGroup].test
);
}
: {}
}
}
};

return {
splitChunks: splitChunksConfig,
};
};
234 changes: 234 additions & 0 deletions packages/react-scripts/backpack-addons/splitChunks.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
'use strict';

jest.mock('../config/paths', () => ({
appPackageJson: './test/mockPackage.json',
}));

describe('splitChunks', () => {
const mockData = {
name: 'test',
version: '1.0.0',
'backpack-react-scripts': {},
};

let isEnvDevelopment = true;

beforeEach(() => {
jest.resetModules();
});

test('should return default if no config defined', () => {
jest.doMock('./test/mockPackage.json', () => ({
Copy link
Author

@james-od james-od Oct 11, 2023

Choose a reason for hiding this comment

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

The testing setup here was a bit awkward, open to suggestions as always.
Essentially, splitChunks.js pulls the JSON here:

const paths = require('../config/paths');
const appPackageJson = require(paths.appPackageJson);
const bpkReactScriptsConfig = appPackageJson['backpack-react-scripts'] || {};

So in the test file, we're mocking the result of paths to be {appPackageJson: './test/mockPackage.json'}.
The exact path is arbitrary, only matters that we stay consistent.

Then the second line will resolve as const appPackageJson = require('./test/mockPackage.json');.

So then in every test we can mock out that module using doMock. We need doMock instead of .mock such that the calls aren't hoisted and overriden.

We can't (or at least I can't) use the standard jest mocked module pattern because the module returns a JSON object, not a function.

https://jestjs.io/docs/jest-object#jestdomockmodulename-factory-options

...mockData,
'backpack-react-scripts': {},
}));
const splitChunks = require('../backpack-addons/splitChunks');

let res = splitChunks(isEnvDevelopment);

expect(res).toEqual({ splitChunks: {} });
});

test('should apply basic defaults if automatic chunking enabled without vendors regex', () => {
jest.doMock('./test/mockPackage.json', () => ({
...mockData,
'backpack-react-scripts': {
enableAutomaticChunking: true,
},
}));
const splitChunks = require('../backpack-addons/splitChunks');

let res = splitChunks(isEnvDevelopment);

expect(res).toEqual({
splitChunks: { chunks: 'all', name: true, cacheGroups: {} },
});
});

test('should return empty if automatic chunking false and no other config is defined', () => {
jest.doMock('./test/mockPackage.json', () => ({
...mockData,
'backpack-react-scripts': {
enableAutomaticChunking: false,
},
}));
const splitChunks = require('../backpack-addons/splitChunks');

let res = splitChunks(isEnvDevelopment);

expect(res).toEqual({ splitChunks: {} });
});

test('should apply basic defaults and cacheGroup with vendors RegExp when automatic chunking enabled and vendors regex provided', () => {
jest.doMock('./test/mockPackage.json', () => ({
...mockData,
'backpack-react-scripts': {
enableAutomaticChunking: true,
vendorsChunkRegex: '[\\/]node_modules[\\/]',
},
}));
const splitChunks = require('../backpack-addons/splitChunks');

let res = splitChunks(isEnvDevelopment);

expect(res).toEqual({
splitChunks: {
chunks: 'all',
name: true,
cacheGroups: { vendors: { test: expect.any(RegExp) } },
},
});
});

test('should return empty when automatic chunking disabled and vendors regex provided', () => {
jest.doMock('./test/mockPackage.json', () => ({
...mockData,
'backpack-react-scripts': {
enableAutomaticChunking: false,
vendorsChunkRegex: '[\\/]node_modules[\\/]',
},
}));
const splitChunks = require('../backpack-addons/splitChunks');

let res = splitChunks(isEnvDevelopment);

expect(res).toEqual({ splitChunks: {} });
});

test('should ignore custom config when automatic chunking enabled and splitChunksConfig is also defined', () => {
jest.doMock('./test/mockPackage.json', () => ({
...mockData,
'backpack-react-scripts': {
enableAutomaticChunking: true,
splitChunksConfig: {
cacheGroups: {
vendors: {
test: '[\\/]node_modules[\\/]',
},
someCustomChunk: {
test: '[\\/]some_regex[\\/]',
priority: 100,
chunks: 'all',
name: 'someCustomChunk',
},
},
},
},
}));
const splitChunks = require('../backpack-addons/splitChunks');

let res = splitChunks(isEnvDevelopment);

expect(res).toEqual({
splitChunks: { chunks: 'all', name: true, cacheGroups: {} },
});
});

test('should not ignore custom config when automatic chunking disabled and splitChunksConfig is defined', () => {
jest.doMock('./test/mockPackage.json', () => ({
...mockData,
'backpack-react-scripts': {
enableAutomaticChunking: false,
splitChunksConfig: {
chunks: 'all',
cacheGroups: {
vendors: {
test: '[\\/]node_modules[\\/]',
},
},
},
},
}));
const splitChunks = require('../backpack-addons/splitChunks');

let res = splitChunks(isEnvDevelopment);

expect(res).toEqual({
splitChunks: {
chunks: 'all',
name: true,
cacheGroups: {
vendors: {
test: expect.any(RegExp),
},
},
},
});
});

test('should apply only the name field when splitChunks is empty', () => {
jest.doMock('./test/mockPackage.json', () => ({
...mockData,
'backpack-react-scripts': {
enableAutomaticChunking: false,
splitChunksConfig: {},
},
}));
const splitChunks = require('../backpack-addons/splitChunks');

let res = splitChunks(isEnvDevelopment);

expect(res).toEqual({ splitChunks: { name: true } });
});

test('should apply Regexes when multiple cacheGroups are applied', () => {
jest.doMock('./test/mockPackage.json', () => ({
...mockData,
'backpack-react-scripts': {
enableAutomaticChunking: false,
splitChunksConfig: {
chunks: 'all',
cacheGroups: {
vendors: {
test: '[\\/]node_modules[\\/]',
},
someCustomChunk: {
test: '[\\/]some_regex[\\/]',
priority: 100,
chunks: 'all',
name: 'someCustomChunk',
},
},
},
},
}));
const splitChunks = require('../backpack-addons/splitChunks');

let res = splitChunks(isEnvDevelopment);

expect(res).toEqual({
splitChunks: {
chunks: 'all',
name: true,
cacheGroups: {
vendors: {
test: expect.any(RegExp),
},
someCustomChunk: {
test: expect.any(RegExp),
priority: 100,
chunks: 'all',
name: 'someCustomChunk',
},
},
},
});
});

test('should apply isEnvDevelopment boolean as name value', () => {
let isEnvDevelopment = false;
jest.doMock('./test/mockPackage.json', () => ({
...mockData,
'backpack-react-scripts': {
enableAutomaticChunking: true,
},
}));
const splitChunks = require('../backpack-addons/splitChunks');

let res = splitChunks(isEnvDevelopment);

expect(res).toEqual({
splitChunks: { chunks: 'all', name: false, cacheGroups: {} },
});
});
});
3 changes: 3 additions & 0 deletions packages/react-scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
"utils",
"backpack-addons"
],
"scripts": {
"test:addons": "jest --testPathPattern=backpack-addons"
Copy link
Author

@james-od james-od Oct 11, 2023

Choose a reason for hiding this comment

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

This will also run babelIncludePrefixes.test.js, the only other test in the folder other than the kitchen sink e2e block. If theres an alternative existing test pattern I'm missing lmk.

},
"bin": {
"react-scripts": "./bin/react-scripts.js"
},
Expand Down
Loading