From 91c961e18890c114738479cfe6681efd30c6b84a Mon Sep 17 00:00:00 2001 From: james-od Date: Thu, 12 Oct 2023 09:30:10 +0100 Subject: [PATCH] [ST-5243] Make splitChunks configurable (#167) * [ST-5243] Make splitChunks configurable * Add tests --------- Co-authored-by: James ODonnell --- packages/react-scripts/README.md | 1 + .../react-scripts/backpack-addons/README.md | 1 + .../backpack-addons/splitChunks.js | 99 ++++++-- .../backpack-addons/splitChunks.test.js | 234 ++++++++++++++++++ packages/react-scripts/package.json | 3 + 5 files changed, 322 insertions(+), 16 deletions(-) create mode 100644 packages/react-scripts/backpack-addons/splitChunks.test.js diff --git a/packages/react-scripts/README.md b/packages/react-scripts/README.md index dbea2fae2e..4963d40d39 100644 --- a/packages/react-scripts/README.md +++ b/packages/react-scripts/README.md @@ -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. diff --git a/packages/react-scripts/backpack-addons/README.md b/packages/react-scripts/backpack-addons/README.md index f5c4431c08..1f0e67857b 100644 --- a/packages/react-scripts/backpack-addons/README.md +++ b/packages/react-scripts/backpack-addons/README.md @@ -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.
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.
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.
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).
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 diff --git a/packages/react-scripts/backpack-addons/splitChunks.js b/packages/react-scripts/backpack-addons/splitChunks.js index 6859170161..b3d3a9a59e 100644 --- a/packages/react-scripts/backpack-addons/splitChunks.js +++ b/packages/react-scripts/backpack-addons/splitChunks.js @@ -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 + ); } - : {} + } } -}; \ No newline at end of file + + return { + splitChunks: splitChunksConfig, + }; +}; diff --git a/packages/react-scripts/backpack-addons/splitChunks.test.js b/packages/react-scripts/backpack-addons/splitChunks.test.js new file mode 100644 index 0000000000..6d945e98a1 --- /dev/null +++ b/packages/react-scripts/backpack-addons/splitChunks.test.js @@ -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', () => ({ + ...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: {} }, + }); + }); +}); diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index fb019a53dd..931a086a6d 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -21,6 +21,9 @@ "utils", "backpack-addons" ], + "scripts": { + "test:addons": "jest --testPathPattern=backpack-addons" + }, "bin": { "react-scripts": "./bin/react-scripts.js" },