diff --git a/lib/options.json b/lib/options.json index 5edd121159..781a166fa4 100644 --- a/lib/options.json +++ b/lib/options.json @@ -228,6 +228,13 @@ { "type": "boolean" }, + { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, { "instanceof": "Function" } @@ -238,6 +245,13 @@ { "type": "boolean" }, + { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, { "instanceof": "Function" } @@ -395,8 +409,8 @@ "hot": "should be {Boolean|String} (https://webpack.js.org/configuration/dev-server/#devserverhot)", "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)", - "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)", "liveReload": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserverlivereload)", "onAfterSetupMiddleware": "should be {Function} (https://webpack.js.org/configuration/dev-server/#devserverafter)", "onBeforeSetupMiddleware": "should be {Function} (https://webpack.js.org/configuration/dev-server/#devserverbefore)", diff --git a/lib/utils/DevServerPlugin.js b/lib/utils/DevServerPlugin.js index 312e3a449e..8aef5a2dbe 100644 --- a/lib/utils/DevServerPlugin.js +++ b/lib/utils/DevServerPlugin.js @@ -20,6 +20,13 @@ class DevServerPlugin { * @typedef {(string[] | string | Object)} Entry */ + /** + * Additional entry to add to specific chunk + * @typedef {Object} AdditionalChunkEntry + * @property {Entry} entry + * @property {string[]} [chunks] + */ + /** * Apply the plugin * @param {Object} compiler the compiler instance @@ -66,7 +73,7 @@ class DevServerPlugin { /** * prependEntry Method for webpack 4 * @param {Entry} originalEntry - * @param {Entry} additionalEntries + * @param {AdditionalChunkEntry[]} additionalEntries * @returns {Entry} */ const prependEntry = (originalEntry, additionalEntries) => { @@ -83,8 +90,13 @@ class DevServerPlugin { Object.keys(originalEntry).forEach((key) => { // entry[key] should be a string here + const chunkAdditionalEntries = additionalEntries.filter( + (additionalEntry) => + !additionalEntry.chunks || additionalEntry.chunks.includes(key) + ); + const entryDescription = originalEntry[key]; - clone[key] = prependEntry(entryDescription, additionalEntries); + clone[key] = prependEntry(entryDescription, chunkAdditionalEntries); }); return clone; @@ -93,13 +105,15 @@ class DevServerPlugin { // 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) => additionalEntry.entry + ); [].concat(originalEntry).forEach((newEntry) => { - if (!entriesClone.includes(newEntry)) { - entriesClone.push(newEntry); + if (!newEntries.includes(newEntry)) { + newEntries.push(newEntry); } }); - return entriesClone; + return newEntries; }; /** @@ -112,14 +126,15 @@ class DevServerPlugin { /** * - * @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; }; @@ -138,37 +153,86 @@ class DevServerPlugin { undefined, null, ].includes(compilerOptions.target); - /** @type {Entry} */ - const additionalEntries = checkInject( + /** @type {AdditionalChunkEntry[]} */ + const additionalEntries = []; + + const checkInjectClientResult = checkInject( options.injectClient, compilerOptions, isWebTarget - ) - ? [clientEntry] - : []; + ); + if (checkInjectClientResult) { + additionalEntries.push({ + entry: clientEntry, + chunks: Array.isArray(checkInjectClientResult) + ? checkInjectClientResult + : null, + }); + } - if (hotEntry && checkInject(options.injectHot, compilerOptions, true)) { - additionalEntries.push(hotEntry); + if (hotEntry) { + const checkInjectHotResult = checkInject( + options.injectHot, + compilerOptions, + true + ); + if (checkInjectHotResult) { + additionalEntries.push({ + entry: hotEntry, + chunks: Array.isArray(checkInjectHotResult) + ? checkInjectHotResult + : null, + }); + } } // use a hook to add entries if available if (EntryPlugin) { compiler.hooks.make.tapPromise('DevServerPlugin', (compilation) => Promise.all( - additionalEntries.map( - (entry) => - new Promise((resolve, reject) => { - compilation.addEntry( - compiler.context, - EntryPlugin.createDependency(entry, {}), - {}, // global entry - (err) => { - if (err) return reject(err); - resolve(); - } + additionalEntries.map((additionalChunkEntry) => { + // add entry to existing chunks + if ( + additionalChunkEntry.chunks && + Array.isArray(additionalChunkEntry.chunks) + ) { + let promise = Promise.resolve(); + additionalChunkEntry.chunks.forEach((chunkName) => { + promise = promise.then( + () => + new Promise((resolve, reject) => { + compilation.addEntry( + compiler.context, + EntryPlugin.createDependency( + additionalChunkEntry.entry, + {} + ), + chunkName, + (err) => { + if (err) return reject(err); + resolve(); + } + ); + }) ); - }) - ) + }); + + return promise; + } + + // add new entry + return new Promise((resolve, reject) => { + compilation.addEntry( + compiler.context, + EntryPlugin.createDependency(additionalChunkEntry.entry, {}), + {}, // global entry + (err) => { + if (err) return reject(err); + resolve(); + } + ); + }); + }) ) ); } else { diff --git a/test/__snapshots__/Validation.test.js.snap b/test/__snapshots__/Validation.test.js.snap index f88f0c5255..e353fee5c7 100644 --- a/test/__snapshots__/Validation.test.js.snap +++ b/test/__snapshots__/Validation.test.js.snap @@ -12,9 +12,11 @@ exports[`Validation validation should fail validation for invalid \`hot\` config exports[`Validation validation should fail validation for invalid \`injectHot\` configuration 1`] = ` "Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. - configuration.injectHot should be one of these: - boolean | function + boolean | [string, ...] (should not have fewer than 1 item) | function Details: * configuration.injectHot should be a boolean. + * configuration.injectHot should be an array: + [string, ...] (should not have fewer than 1 item) * configuration.injectHot should be an instance of function." `; diff --git a/test/options.test.js b/test/options.test.js index 95dccc82e0..4b041c03bc 100644 --- a/test/options.test.js +++ b/test/options.test.js @@ -359,11 +359,11 @@ describe('options', () => { ], }, injectClient: { - success: [true, () => {}], + success: [true, ['a'], () => {}], failure: [''], }, injectHot: { - success: [true, () => {}], + success: [true, ['a'], () => {}], failure: [''], }, onListening: { diff --git a/test/server/utils/DevServerPlugin.test.js b/test/server/utils/DevServerPlugin.test.js index b7de5cc00b..bc508f6bc1 100644 --- a/test/server/utils/DevServerPlugin.test.js +++ b/test/server/utils/DevServerPlugin.test.js @@ -17,9 +17,22 @@ describe('DevServerPlugin util', () => { const entries = []; const compilation = { - addEntry(_context, dep, _options, cb) { + addEntry(_context, dep, _optionsOrName, cb) { if (!dep.loc.name) { - entries.push(dep.request); + const name = + typeof _optionsOrName === 'object' + ? _optionsOrName.name + : _optionsOrName; + + if (name && entryOption[name]) { + const entry = entryOption[name]; + entries[name] = { + ...entry, + import: [dep.request, ...entry.import], + }; + } else { + entries.push(dep.request); + } } cb(); }, @@ -39,8 +52,7 @@ describe('DevServerPlugin util', () => { entries.push(...entryOption.main.import); } // merge named exports into entries - Object.assign(entries, entryOption); - return entries; + return Object.assign([], entryOption, entries); } return entryOption; } @@ -621,6 +633,53 @@ describe('DevServerPlugin util', () => { ); }); + it('should allows selecting chunks to inline the client into', async () => { + const webpackOptions = [ + Object.assign({}, config, { + entry: { + chunk1: ['./foo.js'], + chunk2: './foo.js', + chunk3: './foo.js', + }, + }), + ]; + const compiler = webpack(webpackOptions); + + const devServerOptions = { + injectClient: ['chunk1', 'chunk3'], + transportMode: { + server: 'sockjs', + client: 'sockjs', + }, + }; + + await Promise.all( + // eslint-disable-next-line no-shadow + compiler.compilers.map((compiler) => { + const plugin = new DevServerPlugin(devServerOptions); + plugin.apply(compiler); + return getEntries(compiler).then((entries) => { + expect(Object.keys(entries).length).toEqual(3); + expect(entries.chunk1.import.length).toEqual(2); + expect(entries.chunk2.import.length).toEqual(1); + expect(entries.chunk3.import.length).toEqual(2); + + expect( + normalize(entries.chunk1.import[0]).indexOf( + 'client/default/index.js?' + ) !== -1 + ).toBeTruthy(); + expect(normalize(entries.chunk2.import[0])).toEqual('./foo.js'); + expect( + normalize(entries.chunk3.import[0]).indexOf( + 'client/default/index.js?' + ) !== -1 + ).toBeTruthy(); + }); + }) + ); + }); + it('should prepends the hot runtime to all targets by default (when hot)', async () => { const webpackOptions = [ Object.assign({ target: 'web' }, config),