diff --git a/lib/utils/DevServerEntryPlugin.js b/lib/utils/DevServerEntryPlugin.js new file mode 100644 index 0000000000..ee6eebb1b5 --- /dev/null +++ b/lib/utils/DevServerEntryPlugin.js @@ -0,0 +1,59 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Based on webpack/lib/DynamicEntryPlugin + Author Naoyuki Kanezawa @nkzawa +*/ + +'use strict'; + +const webpack = require('webpack'); + +// @ts-ignore +const EntryPlugin = webpack.EntryPlugin; + +class DevServerEntryPlugin { + /** + * @param {string} context context path + * @param {string[]} entries entry paths + * @param {?Object | string} options entry options + */ + constructor(context, entries, options) { + if (!EntryPlugin) { + throw new Error( + 'DevServerEntryPlugin is supported in webpack 5 or greater' + ); + } + + this.context = context; + this.entries = entries; + this.options = options || ''; + } + + /** + * Apply the plugin + * @param {Object} compiler the compiler instance + * @returns {void} + */ + apply(compiler) { + compiler.hooks.make.tapPromise('DevServerEntryPlugin', (compilation) => + Promise.all( + this.entries.map( + (entry) => + new Promise((resolve, reject) => { + compilation.addEntry( + this.context, + EntryPlugin.createDependency(entry, this.options), + this.options, + (err) => { + if (err) return reject(err); + resolve(); + } + ); + }) + ) + ) + ); + } +} + +module.exports = DevServerEntryPlugin; diff --git a/lib/utils/addEntries.js b/lib/utils/addEntries.js index 6c7216adfa..c04cee5177 100644 --- a/lib/utils/addEntries.js +++ b/lib/utils/addEntries.js @@ -2,6 +2,7 @@ const webpack = require('webpack'); const createDomain = require('./createDomain'); +const DevServerEntryPlugin = require('./DevServerEntryPlugin'); /** * A Entry, it can be of type string or string[] or Object @@ -10,12 +11,12 @@ const createDomain = require('./createDomain'); /** * Add entries Method - * @param {?Object} config - Webpack config + * @param {?Object} compiler - Webpack compiler * @param {?Object} options - Dev-Server options * @param {?Object} server * @returns {void} */ -function addEntries(config, options, server) { +function addEntries(compiler, options, server) { // we're stubbing the app in this method as it's static and doesn't require // a server to be supplied. createDomain requires an app with the // address() signature. @@ -54,7 +55,7 @@ function addEntries(config, options, server) { hotEntry = require.resolve('webpack/hot/dev-server'); } /** - * prependEntry Method + * prependEntry Method for webpack 4 * @param {Entry} originalEntry * @param {Entry} additionalEntries * @returns {Entry} @@ -74,13 +75,7 @@ function addEntries(config, options, server) { Object.keys(originalEntry).forEach((key) => { // entry[key] should be a string here const entryDescription = originalEntry[key]; - if (typeof entryDescription === 'object' && entryDescription.import) { - clone[key] = Object.assign({}, entryDescription, { - import: prependEntry(entryDescription.import, additionalEntries), - }); - } else { - clone[key] = prependEntry(entryDescription, additionalEntries); - } + clone[key] = prependEntry(entryDescription, additionalEntries); }); return clone; @@ -120,8 +115,11 @@ function addEntries(config, options, server) { return defaultValue; }; + const compilers = compiler.compilers || [compiler]; + // eslint-disable-next-line no-shadow - [].concat(config).forEach((config) => { + compilers.forEach((compiler) => { + const config = compiler.options; /** @type {Boolean} */ const webTarget = [ 'web', @@ -145,7 +143,29 @@ function addEntries(config, options, server) { additionalEntries.push(hotEntry); } - config.entry = prependEntry(config.entry || './src', additionalEntries); + // use a plugin to add entries if available + // @ts-ignore + if (webpack.EntryPlugin) { + // use existing plugin if possible + let entryPlugin = config.plugins.find( + (plugin) => plugin.constructor.name === 'DevServerEntryPlugin' + ); + + if (entryPlugin) { + entryPlugin.entries = additionalEntries; + } else { + entryPlugin = new DevServerEntryPlugin( + compiler.context, + additionalEntries, + {} // global entry + ); + config.plugins.push(entryPlugin); + entryPlugin.apply(compiler); + } + } else { + config.entry = prependEntry(config.entry || './src', additionalEntries); + compiler.hooks.entryOption.call(config.context, config.entry); + } if (options.hot || options.hot === 'only') { config.plugins = config.plugins || []; diff --git a/lib/utils/updateCompiler.js b/lib/utils/updateCompiler.js index 788af5f203..ca3d9397d3 100644 --- a/lib/utils/updateCompiler.js +++ b/lib/utils/updateCompiler.js @@ -18,19 +18,15 @@ function updateCompiler(compiler, options) { const compilers = []; const compilersWithoutHMR = []; - let webpackConfig; if (compiler.compilers) { - webpackConfig = []; // eslint-disable-next-line no-shadow compiler.compilers.forEach((compiler) => { - webpackConfig.push(compiler.options); compilers.push(compiler); if (!findHMRPlugin(compiler.options)) { compilersWithoutHMR.push(compiler); } }); } else { - webpackConfig = compiler.options; compilers.push(compiler); if (!findHMRPlugin(compiler.options)) { compilersWithoutHMR.push(compiler); @@ -42,12 +38,9 @@ function updateCompiler(compiler, options) { // the changes we are making to the compiler // important: this relies on the fact that addEntries now // prevents duplicate new entries. - addEntries(webpackConfig, options); + addEntries(compiler, options); // eslint-disable-next-line no-shadow compilers.forEach((compiler) => { - const config = compiler.options; - compiler.hooks.entryOption.call(config.context, config.entry); - const providePlugin = new webpack.ProvidePlugin({ __webpack_dev_server_client__: getSocketClientPath(options), }); diff --git a/package-lock.json b/package-lock.json index 6524c8b216..c39e55c9fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15732,6 +15732,12 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", diff --git a/package.json b/package.json index 534b4696fa..f4711665c1 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ "npm-run-all": "^4.1.5", "prettier": "^2.0.5", "puppeteer": "^5.2.1", + "require-from-string": "^2.0.2", "rimraf": "^3.0.2", "standard-version": "^9.0.0", "style-loader": "^1.2.1", diff --git a/test/fixtures/module-federation-config/entry1.js b/test/fixtures/module-federation-config/entry1.js new file mode 100644 index 0000000000..2f7d8f60a2 --- /dev/null +++ b/test/fixtures/module-federation-config/entry1.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = 'entry1'; diff --git a/test/fixtures/module-federation-config/entry2.js b/test/fixtures/module-federation-config/entry2.js new file mode 100644 index 0000000000..1e5846db72 --- /dev/null +++ b/test/fixtures/module-federation-config/entry2.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = 'entry2'; diff --git a/test/fixtures/module-federation-config/webpack.config.js b/test/fixtures/module-federation-config/webpack.config.js new file mode 100644 index 0000000000..78f9e3b330 --- /dev/null +++ b/test/fixtures/module-federation-config/webpack.config.js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = { + mode: 'development', + target: 'node', + context: __dirname, + entry: ['./entry1.js', './entry2.js'], + output: { + path: '/', + libraryTarget: 'umd', + }, + infrastructureLogging: { + level: 'warn', + }, +}; diff --git a/test/fixtures/module-federation-config/webpack.multi.config.js b/test/fixtures/module-federation-config/webpack.multi.config.js new file mode 100644 index 0000000000..46935865a2 --- /dev/null +++ b/test/fixtures/module-federation-config/webpack.multi.config.js @@ -0,0 +1,17 @@ +'use strict'; + +module.exports = [ + { + mode: 'development', + target: 'node', + context: __dirname, + entry: ['./entry1.js', './entry2.js'], + output: { + path: '/', + libraryTarget: 'umd', + }, + infrastructureLogging: { + level: 'warn', + }, + }, +]; diff --git a/test/fixtures/module-federation-config/webpack.object-entry.config.js b/test/fixtures/module-federation-config/webpack.object-entry.config.js new file mode 100644 index 0000000000..daa32d0e87 --- /dev/null +++ b/test/fixtures/module-federation-config/webpack.object-entry.config.js @@ -0,0 +1,18 @@ +'use strict'; + +module.exports = { + mode: 'development', + target: 'node', + context: __dirname, + entry: { + foo: './entry1.js', + main: ['./entry1.js', './entry2.js'], + }, + output: { + path: '/', + libraryTarget: 'umd', + }, + infrastructureLogging: { + level: 'warn', + }, +}; diff --git a/test/integration/ModuleFederation.test.js b/test/integration/ModuleFederation.test.js new file mode 100644 index 0000000000..678f2758c7 --- /dev/null +++ b/test/integration/ModuleFederation.test.js @@ -0,0 +1,55 @@ +'use strict'; + +const request = require('supertest'); +const requireFromString = require('require-from-string'); +const testServer = require('../helpers/test-server'); +const simpleConfig = require('../fixtures/module-federation-config/webpack.config'); +const objectEntryConfig = require('../fixtures/module-federation-config/webpack.object-entry.config'); +const multiConfig = require('../fixtures/module-federation-config/webpack.multi.config'); +const port = require('../ports-map').ModuleFederation; + +describe('module federation', () => { + describe.each([ + ['simple multi-entry config', simpleConfig], + ['object multi-entry config', objectEntryConfig], + ['multi compiler config', multiConfig], + ])('%s', (title, config) => { + let server; + let req; + + beforeAll((done) => { + server = testServer.start(config, { port }, done); + req = request(server.app); + }); + + afterAll(testServer.close); + + it('should use the last entry export', async () => { + const { text, statusCode } = await req.get('/main.js'); + expect(statusCode).toEqual(200); + expect(text).toContain('entry1'); + + let exports; + expect(() => { + exports = requireFromString(text); + }).not.toThrow(); + + expect(exports).toEqual('entry2'); + }); + + if (title === 'object multi-entry config') { + it('should support the named entry export', async () => { + const { text, statusCode } = await req.get('/foo.js'); + expect(statusCode).toEqual(200); + expect(text).not.toContain('entry2'); + + let exports; + expect(() => { + exports = requireFromString(text); + }).not.toThrow(); + + expect(exports).toEqual('entry1'); + }); + } + }); +}); diff --git a/test/ports-map.js b/test/ports-map.js index 46be5ef5e8..8e018b652b 100644 --- a/test/ports-map.js +++ b/test/ports-map.js @@ -44,6 +44,7 @@ const portsList = { 'static-publicPath-option': 1, 'contentBasePublicPath-option': 1, bundle: 1, + ModuleFederation: 1, }; let startPort = 8089; diff --git a/test/server/Server.test.js b/test/server/Server.test.js index 536d0aedde..f894290e4c 100644 --- a/test/server/Server.test.js +++ b/test/server/Server.test.js @@ -4,6 +4,7 @@ const { relative, sep } = require('path'); const webpack = require('webpack'); const sockjs = require('sockjs/lib/transport'); const Server = require('../../lib/Server'); +const DevServerEntryPlugin = require('../../lib/utils/DevServerEntryPlugin'); const config = require('../fixtures/simple-config/webpack.config'); const port = require('../ports-map').Server; const isWebpack5 = require('../helpers/isWebpack5'); @@ -25,6 +26,27 @@ describe('Server', () => { }); describe('addEntries', () => { + let entries; + + function getEntries(server) { + if (isWebpack5) { + server.middleware.context.compiler.hooks.afterEmit.tap( + 'webpack-dev-server', + (compilation) => { + const mainDeps = compilation.entries.get('main').dependencies; + const globalDeps = compilation.globalEntry.dependencies; + entries = globalDeps.concat(mainDeps).map((dep) => { + return relative('.', dep.request).split(sep); + }); + } + ); + } else { + entries = server.middleware.context.compiler.options.entry.map((p) => { + return relative('.', p).split(sep); + }); + } + } + it('add hot option', (done) => { const compiler = webpack(config); const server = new Server( @@ -34,25 +56,16 @@ describe('Server', () => { }) ); - let entries; + getEntries(server); + const plugins = server.middleware.context.compiler.options.plugins; + expect(plugins).toContainEqual(new webpack.HotModuleReplacementPlugin()); if (isWebpack5) { - entries = server.middleware.context.compiler.options.entry.main.import.map( - (p) => { - return relative('.', p).split(sep); - } - ); - } else { - entries = server.middleware.context.compiler.options.entry.map((p) => { - return relative('.', p).split(sep); - }); + expect(plugins[0]).toBeInstanceOf(DevServerEntryPlugin); } - expect(entries).toMatchSnapshot(); - expect(server.middleware.context.compiler.options.plugins).toEqual([ - new webpack.HotModuleReplacementPlugin(), - ]); compiler.hooks.done.tap('webpack-dev-server', () => { + expect(entries).toMatchSnapshot(); server.close(done); }); @@ -68,26 +81,16 @@ describe('Server', () => { }) ); - let entries; + getEntries(server); + const plugins = server.middleware.context.compiler.options.plugins; + expect(plugins).toContainEqual(new webpack.HotModuleReplacementPlugin()); if (isWebpack5) { - entries = server.middleware.context.compiler.options.entry.main.import.map( - (p) => { - return relative('.', p).split(sep); - } - ); - } else { - entries = server.middleware.context.compiler.options.entry.map((p) => { - return relative('.', p).split(sep); - }); + expect(plugins[0]).toBeInstanceOf(DevServerEntryPlugin); } - expect(entries).toMatchSnapshot(); - expect(server.middleware.context.compiler.options.plugins).toEqual([ - new webpack.HotModuleReplacementPlugin(), - ]); - compiler.hooks.done.tap('webpack-dev-server', () => { + expect(entries).toMatchSnapshot(); server.close(done); }); diff --git a/test/server/utils/addEntries.test.js b/test/server/utils/addEntries.test.js index f600b7c389..41ad67aec1 100644 --- a/test/server/utils/addEntries.test.js +++ b/test/server/utils/addEntries.test.js @@ -3,6 +3,7 @@ const path = require('path'); const webpack = require('webpack'); const addEntries = require('../../../lib/utils/addEntries'); +const isWebpack5 = require('../../helpers/isWebpack5'); const config = require('./../../fixtures/simple-config/webpack.config'); const configEntryAsFunction = require('./../../fixtures/entry-as-function/webpack.config'); const configEntryAsDescriptor = require('./../../fixtures/entry-as-descriptor/webpack.config'); @@ -10,36 +11,64 @@ const configEntryAsDescriptor = require('./../../fixtures/entry-as-descriptor/we const normalize = (entry) => entry.split(path.sep).join('/'); describe('addEntries util', () => { + function getEntries(compiler) { + const entryOption = compiler.options.entry; + if (isWebpack5) { + const entries = []; + const entryPlugin = compiler.options.plugins.find( + (plugin) => plugin.constructor.name === 'DevServerEntryPlugin' + ); + if (entryPlugin) { + entries.push(...entryPlugin.entries); + } + + // normalize entry descriptors + if (typeof entryOption === 'function') { + // merge entries into the dynamic entry function + Object.assign(entryOption, entries); + return entryOption; + } else if (entryOption.main) { + entries.push(...entryOption.main.import); + } + // merge named exports into entries + Object.assign(entries, entryOption); + return entries; + } + return entryOption; + } + it('should adds devServer entry points to a single entry point', () => { const webpackOptions = Object.assign({}, config); + const compiler = webpack(webpackOptions); const devServerOptions = {}; - addEntries(webpackOptions, devServerOptions); + addEntries(compiler, devServerOptions); + const entries = getEntries(compiler); - expect(webpackOptions.entry.length).toEqual(2); + expect(entries.length).toEqual(2); expect( - normalize(webpackOptions.entry[0]).indexOf('client/default/index.js?') !== - -1 + normalize(entries[0]).indexOf('client/default/index.js?') !== -1 ).toBeTruthy(); - expect(normalize(webpackOptions.entry[1])).toEqual('./foo.js'); + expect(normalize(entries[1])).toEqual('./foo.js'); }); it('should adds devServer entry points to a multi-module entry point', () => { const webpackOptions = Object.assign({}, config, { entry: ['./foo.js', './bar.js'], }); + const compiler = webpack(webpackOptions); const devServerOptions = {}; - addEntries(webpackOptions, devServerOptions); + addEntries(compiler, devServerOptions); + const entries = getEntries(compiler); - expect(webpackOptions.entry.length).toEqual(3); + expect(entries.length).toEqual(3); expect( - normalize(webpackOptions.entry[0]).indexOf('client/default/index.js?') !== - -1 + normalize(entries[0]).indexOf('client/default/index.js?') !== -1 ).toBeTruthy(); - expect(webpackOptions.entry[1]).toEqual('./foo.js'); - expect(webpackOptions.entry[2]).toEqual('./bar.js'); + expect(entries[1]).toEqual('./foo.js'); + expect(entries[2]).toEqual('./bar.js'); }); it('should adds devServer entry points to a multi entry point object', () => { @@ -49,30 +78,43 @@ describe('addEntries util', () => { bar: './bar.js', }, }); + const compiler = webpack(webpackOptions); const devServerOptions = {}; - addEntries(webpackOptions, devServerOptions); + addEntries(compiler, devServerOptions); + const entries = getEntries(compiler); + + if (isWebpack5) { + expect(entries.length).toEqual(1); + expect( + normalize(entries[0]).indexOf('client/default/index.js?') !== -1 + ).toBeTruthy(); - expect(webpackOptions.entry.foo.length).toEqual(2); + expect(entries.foo.import.length).toEqual(1); + expect(entries.foo.import[0]).toEqual('./foo.js'); + expect(entries.bar.import[0]).toEqual('./bar.js'); + } else { + expect(entries.foo.length).toEqual(2); - expect( - normalize(webpackOptions.entry.foo[0]).indexOf( - 'client/default/index.js?' - ) !== -1 - ).toBeTruthy(); - expect(webpackOptions.entry.foo[1]).toEqual('./foo.js'); - expect(webpackOptions.entry.bar[1]).toEqual('./bar.js'); + expect( + normalize(entries.foo[0]).indexOf('client/default/index.js?') !== -1 + ).toBeTruthy(); + expect(entries.foo[1]).toEqual('./foo.js'); + expect(entries.bar[1]).toEqual('./bar.js'); + } }); it('should set defaults to src if no entry point is given', () => { const webpackOptions = {}; + const compiler = webpack(webpackOptions); const devServerOptions = {}; - addEntries(webpackOptions, devServerOptions); + addEntries(compiler, devServerOptions); + const entries = getEntries(compiler); - expect(webpackOptions.entry.length).toEqual(2); - expect(webpackOptions.entry[1]).toEqual('./src'); + expect(entries.length).toEqual(2); + expect(entries[1]).toEqual('./src'); }); it('should preserves dynamic entry points', (done) => { @@ -84,21 +126,30 @@ describe('addEntries util', () => { return `./src-${i}.js`; }, }; + const compiler = webpack(webpackOptions); const devServerOptions = {}; - addEntries(webpackOptions, devServerOptions); + addEntries(compiler, devServerOptions); + const entries = getEntries(compiler); - expect(typeof webpackOptions.entry).toEqual('function'); + expect(typeof entries).toEqual('function'); - webpackOptions - .entry() + entries() .then((entryFirstRun) => - webpackOptions.entry().then((entrySecondRun) => { - expect(entryFirstRun.length).toEqual(2); - expect(entryFirstRun[1]).toEqual('./src-1.js'); - - expect(entrySecondRun.length).toEqual(2); - expect(entrySecondRun[1]).toEqual('./src-2.js'); + entries().then((entrySecondRun) => { + if (isWebpack5) { + expect(entryFirstRun.main.import.length).toEqual(1); + expect(entryFirstRun.main.import[0]).toEqual('./src-1.js'); + + expect(entrySecondRun.main.import.length).toEqual(1); + expect(entrySecondRun.main.import[0]).toEqual('./src-2.js'); + } else { + expect(entryFirstRun.length).toEqual(2); + expect(entryFirstRun[1]).toEqual('./src-1.js'); + + expect(entrySecondRun.length).toEqual(2); + expect(entrySecondRun[1]).toEqual('./src-2.js'); + } done(); }) ) @@ -115,22 +166,31 @@ describe('addEntries util', () => { resolve(`./src-${i}.js`); }), }; + const compiler = webpack(webpackOptions); const devServerOptions = {}; - addEntries(webpackOptions, devServerOptions); + addEntries(compiler, devServerOptions); + const entries = getEntries(compiler); - expect(typeof webpackOptions.entry).toEqual('function'); + expect(typeof entries).toEqual('function'); - webpackOptions - .entry() + entries() .then((entryFirstRun) => - webpackOptions.entry().then((entrySecondRun) => { - expect(entryFirstRun.length).toEqual(2); - expect(entryFirstRun[1]).toEqual('./src-1.js'); - - expect(entrySecondRun.length).toEqual(2); - expect(entrySecondRun[1]).toEqual('./src-2.js'); + entries().then((entrySecondRun) => { + if (isWebpack5) { + expect(entryFirstRun.main.import.length).toEqual(1); + expect(entryFirstRun.main.import[0]).toEqual('./src-1.js'); + + expect(entrySecondRun.main.import.length).toEqual(1); + expect(entrySecondRun.main.import[0]).toEqual('./src-2.js'); + } else { + expect(entryFirstRun.length).toEqual(2); + expect(entryFirstRun[1]).toEqual('./src-1.js'); + + expect(entrySecondRun.length).toEqual(2); + expect(entrySecondRun[1]).toEqual('./src-2.js'); + } done(); }) ) @@ -143,14 +203,16 @@ describe('addEntries util', () => { app: './app.js', }, }); + const compiler = webpack(webpackOptions); const devServerOptions = { hot: true, }; - addEntries(webpackOptions, devServerOptions); + addEntries(compiler, devServerOptions); + const entries = getEntries(compiler); - const hotClientScript = webpackOptions.entry.app[1]; + const hotClientScript = (isWebpack5 ? entries : entries.app)[1]; expect( normalize(hotClientScript).includes('webpack/hot/dev-server') @@ -164,14 +226,16 @@ describe('addEntries util', () => { app: './app.js', }, }); + const compiler = webpack(webpackOptions); const devServerOptions = { hot: 'only', }; - addEntries(webpackOptions, devServerOptions); + addEntries(compiler, devServerOptions); + const entries = getEntries(compiler); - const hotClientScript = webpackOptions.entry.app[1]; + const hotClientScript = (isWebpack5 ? entries : entries.app)[1]; expect( normalize(hotClientScript).includes('webpack/hot/only-dev-server') @@ -181,18 +245,20 @@ describe('addEntries util', () => { it("should doesn't add the HMR plugin if not hot and no plugins", () => { const webpackOptions = Object.assign({}, config); + const compiler = webpack(webpackOptions); const devServerOptions = {}; - addEntries(webpackOptions, devServerOptions); + addEntries(compiler, devServerOptions); expect('plugins' in webpackOptions).toBeFalsy(); }); it("should doesn't add the HMR plugin if not hot and empty plugins", () => { const webpackOptions = Object.assign({}, config, { plugins: [] }); + const compiler = webpack(webpackOptions); const devServerOptions = {}; - addEntries(webpackOptions, devServerOptions); + addEntries(compiler, devServerOptions); expect(webpackOptions.plugins).toEqual([]); }); @@ -203,9 +269,10 @@ describe('addEntries util', () => { const webpackOptions = Object.assign({}, config, { plugins: [existingPlugin1, existingPlugin2], }); + const compiler = webpack(webpackOptions); const devServerOptions = {}; - addEntries(webpackOptions, devServerOptions); + addEntries(compiler, devServerOptions); expect(webpackOptions.plugins).toEqual([existingPlugin1, existingPlugin2]); }); @@ -215,25 +282,27 @@ describe('addEntries util', () => { const webpackOptions = Object.assign({}, config, { plugins: [existingPlugin], }); + const compiler = webpack(webpackOptions); const devServerOptions = { hot: true }; - addEntries(webpackOptions, devServerOptions); + addEntries(compiler, devServerOptions); - expect(webpackOptions.plugins).toEqual([ - existingPlugin, - new webpack.HotModuleReplacementPlugin(), - ]); + expect(compiler.options.plugins).toContainEqual(existingPlugin); + expect(compiler.options.plugins).toContainEqual( + new webpack.HotModuleReplacementPlugin() + ); }); it('should adds the HMR plugin if hot-only', () => { const webpackOptions = Object.assign({}, config); + const compiler = webpack(webpackOptions); const devServerOptions = { hot: 'only' }; - addEntries(webpackOptions, devServerOptions); + addEntries(compiler, devServerOptions); - expect(webpackOptions.plugins).toEqual([ - new webpack.HotModuleReplacementPlugin(), - ]); + expect(compiler.options.plugins).toContainEqual( + new webpack.HotModuleReplacementPlugin() + ); }); it("should doesn't add the HMR plugin again if it's already there", () => { @@ -241,9 +310,10 @@ describe('addEntries util', () => { const webpackOptions = Object.assign({}, config, { plugins: [new webpack.HotModuleReplacementPlugin(), existingPlugin], }); + const compiler = webpack(webpackOptions); const devServerOptions = { hot: true }; - addEntries(webpackOptions, devServerOptions); + addEntries(compiler, devServerOptions); expect(webpackOptions.plugins).toEqual([ new webpack.HotModuleReplacementPlugin(), @@ -255,14 +325,18 @@ describe('addEntries util', () => { const existingPlugin = new webpack.BannerPlugin('bruce'); // Simulate the inclusion of another webpack's HotModuleReplacementPlugin - class HotModuleReplacementPlugin {} + class HotModuleReplacementPlugin { + // eslint-disable-next-line class-methods-use-this + apply() {} + } const webpackOptions = Object.assign({}, config, { plugins: [new HotModuleReplacementPlugin(), existingPlugin], }); + const compiler = webpack(webpackOptions); const devServerOptions = { hot: true }; - addEntries(webpackOptions, devServerOptions); + addEntries(compiler, devServerOptions); expect(webpackOptions.plugins).toEqual([ // Nothing should be injected @@ -273,14 +347,16 @@ describe('addEntries util', () => { it('should can prevent duplicate entries from successive calls', () => { const webpackOptions = Object.assign({}, config); + const compiler = webpack(webpackOptions); const devServerOptions = { hot: true }; - addEntries(webpackOptions, devServerOptions); - addEntries(webpackOptions, devServerOptions); + addEntries(compiler, devServerOptions); + addEntries(compiler, devServerOptions); + const entries = getEntries(compiler); - expect(webpackOptions.entry.length).toEqual(3); + expect(entries.length).toEqual(3); - const result = webpackOptions.entry.filter((entry) => + const result = entries.filter((entry) => normalize(entry).includes('webpack/hot/dev-server') ); expect(result.length).toEqual(1); @@ -288,22 +364,28 @@ describe('addEntries util', () => { it('should supports entry as Function', () => { const webpackOptions = Object.assign({}, configEntryAsFunction); + const compiler = webpack(webpackOptions); const devServerOptions = {}; - addEntries(webpackOptions, devServerOptions); + addEntries(compiler, devServerOptions); + const entries = getEntries(compiler); - expect(typeof webpackOptions.entry === 'function').toBe(true); + expect(typeof entries === 'function').toBe(true); }); - it('should supports entry as descriptor', () => { + (isWebpack5 ? it : it.skip)('should supports entry as descriptor', () => { const webpackOptions = Object.assign({}, configEntryAsDescriptor); + const compiler = webpack(webpackOptions); const devServerOptions = {}; - addEntries(webpackOptions, devServerOptions); + addEntries(compiler, devServerOptions); + const entries = getEntries(compiler); - expect(typeof webpackOptions.entry === 'object').toBe(true); - expect(typeof webpackOptions.entry.main === 'object').toBe(true); - expect(Array.isArray(webpackOptions.entry.main.import)).toBe(true); + expect(entries.length).toEqual(2); + expect( + normalize(entries[0]).indexOf('client/default/index.js?') !== -1 + ).toBeTruthy(); + expect(normalize(entries[1])).toEqual('./foo.js'); }); it('should only prepends devServer entry points to web targets by default', () => { @@ -315,28 +397,26 @@ describe('addEntries util', () => { Object.assign({ target: 'node-webkit' }, config), Object.assign({ target: 'node' }, config) /* index:5 */, ]; + const compiler = webpack(webpackOptions); const devServerOptions = {}; - addEntries(webpackOptions, devServerOptions); + addEntries(compiler, devServerOptions); // eslint-disable-next-line no-shadow - webpackOptions.forEach((webpackOptions, index) => { + compiler.compilers.forEach((compiler, index) => { + const entries = getEntries(compiler); const expectInline = index !== 5; /* all but the node target */ - expect(webpackOptions.entry.length).toEqual(expectInline ? 2 : 1); + expect(entries.length).toEqual(expectInline ? 2 : 1); if (expectInline) { expect( - normalize(webpackOptions.entry[0]).indexOf( - 'client/default/index.js?' - ) !== -1 + normalize(entries[0]).indexOf('client/default/index.js?') !== -1 ).toBeTruthy(); } - expect(normalize(webpackOptions.entry[expectInline ? 1 : 0])).toEqual( - './foo.js' - ); + expect(normalize(entries[expectInline ? 1 : 0])).toEqual('./foo.js'); }); }); @@ -347,30 +427,28 @@ describe('addEntries util', () => { Object.assign({ name: 'only-include' }, config) /* index:2 */, Object.assign({ target: 'node' }, config), ]; + const compiler = webpack(webpackOptions); const devServerOptions = { injectClient: (compilerConfig) => compilerConfig.name === 'only-include', }; - addEntries(webpackOptions, devServerOptions); + addEntries(compiler, devServerOptions); // eslint-disable-next-line no-shadow - webpackOptions.forEach((webpackOptions, index) => { + compiler.compilers.forEach((compiler, index) => { + const entries = getEntries(compiler); const expectInline = index === 2; /* only the "only-include" compiler */ - expect(webpackOptions.entry.length).toEqual(expectInline ? 2 : 1); + expect(entries.length).toEqual(expectInline ? 2 : 1); if (expectInline) { expect( - normalize(webpackOptions.entry[0]).indexOf( - 'client/default/index.js?' - ) !== -1 + normalize(entries[0]).indexOf('client/default/index.js?') !== -1 ).toBeTruthy(); } - expect(normalize(webpackOptions.entry[expectInline ? 1 : 0])).toEqual( - './foo.js' - ); + expect(normalize(entries[expectInline ? 1 : 0])).toEqual('./foo.js'); }); }); @@ -379,6 +457,7 @@ describe('addEntries util', () => { Object.assign({ target: 'web' }, config), Object.assign({ target: 'node' }, config), ]; + const compiler = webpack(webpackOptions); const devServerOptions = { // disable inlining the client so entry indexes match up @@ -387,17 +466,18 @@ describe('addEntries util', () => { hot: true, }; - addEntries(webpackOptions, devServerOptions); + addEntries(compiler, devServerOptions); // eslint-disable-next-line no-shadow - webpackOptions.forEach((webpackOptions) => { - expect(webpackOptions.entry.length).toEqual(2); + compiler.compilers.forEach((compiler) => { + const entries = getEntries(compiler); + expect(entries.length).toEqual(2); expect( - normalize(webpackOptions.entry[0]).includes('webpack/hot/dev-server') + normalize(entries[0]).includes('webpack/hot/dev-server') ).toBeTruthy(); - expect(normalize(webpackOptions.entry[1])).toEqual('./foo.js'); + expect(normalize(entries[1])).toEqual('./foo.js'); }); }); @@ -406,65 +486,69 @@ describe('addEntries util', () => { Object.assign({ target: 'web' }, config), Object.assign({ target: 'node' }, config), ]; + const compiler = webpack(webpackOptions); const devServerOptions = { injectHot: (compilerConfig) => compilerConfig.target === 'node', hot: true, }; - addEntries(webpackOptions, devServerOptions); + addEntries(compiler, devServerOptions); // node target should have the client runtime but not the hot runtime - const webWebpackOptions = webpackOptions[0]; + const webEntries = getEntries(compiler.compilers[0]); - expect(webWebpackOptions.entry.length).toEqual(2); + expect(webEntries.length).toEqual(2); expect( - normalize(webWebpackOptions.entry[0]).indexOf( - 'client/default/index.js?' - ) !== -1 + normalize(webEntries[0]).indexOf('client/default/index.js?') !== -1 ).toBeTruthy(); - expect(normalize(webWebpackOptions.entry[1])).toEqual('./foo.js'); + expect(normalize(webEntries[1])).toEqual('./foo.js'); // node target should have the hot runtime but not the client runtime - const nodeWebpackOptions = webpackOptions[1]; + const nodeEntries = getEntries(compiler.compilers[1]); - expect(nodeWebpackOptions.entry.length).toEqual(2); + expect(nodeEntries.length).toEqual(2); expect( - normalize(nodeWebpackOptions.entry[0]).includes('webpack/hot/dev-server') + normalize(nodeEntries[0]).includes('webpack/hot/dev-server') ).toBeTruthy(); - expect(normalize(nodeWebpackOptions.entry[1])).toEqual('./foo.js'); + expect(normalize(nodeEntries[1])).toEqual('./foo.js'); }); it('does not use client.path when default', () => { const webpackOptions = Object.assign({}, config); + const compiler = webpack(webpackOptions); const devServerOptions = { client: { path: '/ws', }, }; - addEntries(webpackOptions, devServerOptions); - expect(webpackOptions.entry[0]).not.toContain('&path=/ws'); + addEntries(compiler, devServerOptions); + const entries = getEntries(compiler); + expect(entries[0]).not.toContain('&path=/ws'); }); it('uses custom client.path', () => { const webpackOptions = Object.assign({}, config); + const compiler = webpack(webpackOptions); const devServerOptions = { client: { path: '/custom/path', }, }; - addEntries(webpackOptions, devServerOptions); - expect(webpackOptions.entry[0]).toContain('&path=/custom/path'); + addEntries(compiler, devServerOptions); + const entries = getEntries(compiler); + expect(entries[0]).toContain('&path=/custom/path'); }); it('uses custom client', () => { const webpackOptions = Object.assign({}, config); + const compiler = webpack(webpackOptions); const devServerOptions = { client: { host: 'my.host', @@ -473,9 +557,8 @@ describe('addEntries util', () => { }, }; - addEntries(webpackOptions, devServerOptions); - expect(webpackOptions.entry[0]).toContain( - '&host=my.host&path=/custom/path&port=8080' - ); + addEntries(compiler, devServerOptions); + const entries = getEntries(compiler); + expect(entries[0]).toContain('&host=my.host&path=/custom/path&port=8080'); }); }); diff --git a/test/server/utils/updateCompiler.test.js b/test/server/utils/updateCompiler.test.js index ecb3f3e806..8ba3f7b43d 100644 --- a/test/server/utils/updateCompiler.test.js +++ b/test/server/utils/updateCompiler.test.js @@ -2,6 +2,7 @@ const webpack = require('webpack'); const updateCompiler = require('../../../lib/utils/updateCompiler'); +const DevServerEntryPlugin = require('../../../lib/utils/DevServerEntryPlugin'); const isWebpack5 = require('../../helpers/isWebpack5'); describe('updateCompiler', () => { @@ -38,7 +39,10 @@ describe('updateCompiler', () => { expect(tapsByProvidePlugin).toEqual(1); if (isWebpack5) { - expect(compiler.options.plugins).toEqual([]); + expect(compiler.options.plugins).toHaveLength(1); + expect(compiler.options.plugins[0]).toBeInstanceOf( + DevServerEntryPlugin + ); } else { expect(compiler.options.plugins).toBeUndefined(); } @@ -77,9 +81,14 @@ describe('updateCompiler', () => { expect(compiler.hooks.entryOption.taps.length).toBe(1); expect(tapsByHMR).toEqual(1); expect(tapsByProvidePlugin).toEqual(1); - expect(compiler.options.plugins).toEqual([ - new webpack.HotModuleReplacementPlugin(), - ]); + expect(compiler.options.plugins).toContainEqual( + new webpack.HotModuleReplacementPlugin() + ); + if (isWebpack5) { + expect(compiler.options.plugins[0]).toBeInstanceOf( + DevServerEntryPlugin + ); + } }); }); @@ -117,9 +126,14 @@ describe('updateCompiler', () => { expect(compiler.hooks.entryOption.taps.length).toBe(1); expect(tapsByHMR).toEqual(1); expect(tapsByProvidePlugin).toEqual(1); - expect(compiler.options.plugins).toEqual([ - new webpack.HotModuleReplacementPlugin(), - ]); + expect(compiler.options.plugins).toContainEqual( + new webpack.HotModuleReplacementPlugin() + ); + if (isWebpack5) { + expect(compiler.options.plugins[1]).toBeInstanceOf( + DevServerEntryPlugin + ); + } }); }); @@ -143,7 +157,7 @@ describe('updateCompiler', () => { hot: true, }); - multiCompiler.compilers.forEach((compiler) => { + multiCompiler.compilers.forEach((compiler, index) => { let tapsByHMR = 0; let tapsByProvidePlugin = 0; @@ -158,9 +172,14 @@ describe('updateCompiler', () => { expect(compiler.hooks.entryOption.taps.length).toBe(1); expect(tapsByHMR).toEqual(1); expect(tapsByProvidePlugin).toEqual(1); - expect(compiler.options.plugins).toEqual([ - new webpack.HotModuleReplacementPlugin(), - ]); + expect(compiler.options.plugins).toContainEqual( + new webpack.HotModuleReplacementPlugin() + ); + if (isWebpack5) { + expect(compiler.options.plugins[index]).toBeInstanceOf( + DevServerEntryPlugin + ); + } }); }); });