diff --git a/lib/options.json b/lib/options.json index 43c8d30b94..38ca8a87ae 100644 --- a/lib/options.json +++ b/lib/options.json @@ -160,6 +160,13 @@ { "type": "boolean" }, + { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, { "instanceof": "Function" } @@ -170,6 +177,13 @@ { "type": "boolean" }, + { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, { "instanceof": "Function" } @@ -451,8 +465,8 @@ "http2": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserverhttp2)", "https": "should be {Object|Boolean} (https://webpack.js.org/configuration/dev-server/#devserverhttps)", "index": "should be {String} (https://webpack.js.org/configuration/dev-server/#devserverindex)", - "injectClient": "should be {Boolean|Function} (https://webpack.js.org/configuration/dev-server/#devserverinjectclient)", - "injectHot": "should be {Boolean|Function} (https://webpack.js.org/configuration/dev-server/#devserverinjecthot)", + "injectClient": "should be {Boolean|String[]|Function} (https://webpack.js.org/configuration/dev-server/#devserverinjectclient)", + "injectHot": "should be {Boolean|String[]|Function} (https://webpack.js.org/configuration/dev-server/#devserverinjecthot)", "inline": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserverinline)", "key": "should be {String|Buffer}", "lazy": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserverlazy-)", diff --git a/lib/utils/addEntries.js b/lib/utils/addEntries.js index 7eaac33003..b6dd277d92 100644 --- a/lib/utils/addEntries.js +++ b/lib/utils/addEntries.js @@ -8,6 +8,13 @@ const createDomain = require('./createDomain'); * @typedef {(string[] | string | Object)} Entry */ +/** + * Additional entry to add to specific chunk + * @typedef {Object} AdditionalChunkEntry + * @property {Entry} entry + * @property {string[]} [chunks] + */ + /** * Add entries Method * @param {?Object} config - Webpack config @@ -51,7 +58,7 @@ function addEntries(config, options, server) { /** * prependEntry Method * @param {Entry} originalEntry - * @param {Entry} additionalEntries + * @param {AdditionalChunkEntry[]} additionalEntries * @returns {Entry} */ const prependEntry = (originalEntry, additionalEntries) => { @@ -68,13 +75,17 @@ function addEntries(config, options, server) { Object.keys(originalEntry).forEach((key) => { // entry[key] should be a string here + const chunkAdditionalEntries = additionalEntries.filter((additionalEntry) => { + return (!additionalEntry.chunks || additionalEntry.chunks.includes(key)); + }) + const entryDescription = originalEntry[key]; if (typeof entryDescription === 'object' && entryDescription.import) { clone[key] = Object.assign({}, entryDescription, { - import: prependEntry(entryDescription.import, additionalEntries), + import: prependEntry(entryDescription.import, chunkAdditionalEntries), }); } else { - clone[key] = prependEntry(entryDescription, additionalEntries); + clone[key] = prependEntry(entryDescription, chunkAdditionalEntries); } }); @@ -84,13 +95,15 @@ function addEntries(config, options, server) { // in this case, entry is a string or an array. // make sure that we do not add duplicates. /** @type {Entry} */ - const entriesClone = additionalEntries.slice(0); + const newEntries = additionalEntries.map((additionalEntry) => { + return additionalEntry.entry; + }); [].concat(originalEntry).forEach((newEntry) => { - if (!entriesClone.includes(newEntry)) { - entriesClone.push(newEntry); + if (!newEntries.includes(newEntry)) { + newEntries.push(newEntry); } }); - return entriesClone; + return newEntries; }; /** @@ -98,19 +111,20 @@ function addEntries(config, options, server) { * Description of the option for checkInject method * @typedef {Function} checkInjectOptionsParam * @param {Object} _config - compilerConfig - * @return {Boolean} + * @return {Boolean | string[]} */ /** * - * @param {Boolean | checkInjectOptionsParam} option - inject(Hot|Client) it is Boolean | fn => Boolean + * @param {Boolean | string[] | checkInjectOptionsParam} option - inject(Hot|Client) it is Boolean | fn => Boolean * @param {Object} _config * @param {Boolean} defaultValue - * @return {Boolean} + * @return {Boolean | string[]} */ // eslint-disable-next-line no-shadow const checkInject = (option, _config, defaultValue) => { if (typeof option === 'boolean') return option; + if (Array.isArray(option)) return option; if (typeof option === 'function') return option(_config); return defaultValue; }; @@ -126,17 +140,25 @@ function addEntries(config, options, server) { undefined, // eslint-disable-line null, ].includes(config.target); - /** @type {Entry} */ - const additionalEntries = checkInject( - options.injectClient, - config, - webTarget - ) - ? [clientEntry] - : []; - - if (hotEntry && checkInject(options.injectHot, config, true)) { - additionalEntries.push(hotEntry); + /** @type {AdditionalChunkEntry[]} */ + const additionalEntries = [] + + const checkInjectClientResult = checkInject(options.injectClient, config, webTarget); + if (checkInjectClientResult) { + additionalEntries.push({ + entry: clientEntry, + chunks: Array.isArray(checkInjectClientResult) ? checkInjectClientResult : null + }); + } + + if (hotEntry) { + const checkInjectHotResult = checkInject(options.injectHot, config, true); + if (checkInjectHotResult) { + additionalEntries.push({ + entry: hotEntry, + chunks: Array.isArray(checkInjectHotResult) ? checkInjectHotResult : null + }); + } } config.entry = prependEntry(config.entry || './src', additionalEntries); diff --git a/test/options.test.js b/test/options.test.js index 26b198b41d..22e369ca62 100644 --- a/test/options.test.js +++ b/test/options.test.js @@ -208,11 +208,11 @@ describe('options', () => { failure: [false], }, injectClient: { - success: [true, () => {}], + success: [true, ['a'], () => {}], failure: [''], }, injectHot: { - success: [true, () => {}], + success: [true, ['a'], () => {}], failure: [''], }, inline: { diff --git a/test/server/utils/addEntries.test.js b/test/server/utils/addEntries.test.js index a5a375da2a..cf0d17574a 100644 --- a/test/server/utils/addEntries.test.js +++ b/test/server/utils/addEntries.test.js @@ -366,6 +366,42 @@ describe('addEntries util', () => { }); }); + it('should allows selecting chunks to inline the client into', () => { + const webpackOptions = [ + Object.assign({}, config, { + entry: { + chunk1: './foo.js', + chunk2: './foo.js', + chunk3: './foo.js' + } + }) + ]; + + const devServerOptions = { + injectClient: ['chunk1', 'chunk3'] + }; + + addEntries(webpackOptions, devServerOptions); + + // eslint-disable-next-line no-shadow + webpackOptions.forEach((webpackOptions) => { + expect(Object.keys(webpackOptions.entry).length).toEqual(3); + expect(webpackOptions.entry.chunk1.length).toEqual(2); + expect(webpackOptions.entry.chunk2.length).toEqual(1); + expect(webpackOptions.entry.chunk3.length).toEqual(2); + + expect( + normalize(webpackOptions.entry.chunk1[0]).indexOf('client/index.js?') !== -1 + ).toBeTruthy(); + expect(normalize(webpackOptions.entry.chunk2[0])).toEqual( + './foo.js' + ); + expect( + normalize(webpackOptions.entry.chunk3[0]).indexOf('client/index.js?') !== -1 + ).toBeTruthy(); + }); + }); + it('should prepends the hot runtime to all targets by default (when hot)', () => { const webpackOptions = [ Object.assign({ target: 'web' }, config),