diff --git a/lib/Server.js b/lib/Server.js index 181f9235b9..63a60f7c60 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -865,9 +865,4 @@ class Server { } } -// Export this logic, -// so that other implementations, -// like task-runners can use it -Server.addDevServerEntrypoints = require('./utils/addEntries'); - module.exports = Server; diff --git a/lib/utils/DevServerEntryPlugin.js b/lib/utils/DevServerEntryPlugin.js deleted file mode 100644 index ee6eebb1b5..0000000000 --- a/lib/utils/DevServerEntryPlugin.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - 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/DevServerPlugin.js b/lib/utils/DevServerPlugin.js new file mode 100644 index 0000000000..7891af9a77 --- /dev/null +++ b/lib/utils/DevServerPlugin.js @@ -0,0 +1,210 @@ +'use strict'; + +const webpack = require('webpack'); +const createDomain = require('./createDomain'); +const getSocketClientPath = require('./getSocketClientPath'); + +// @ts-ignore +const EntryPlugin = webpack.EntryPlugin; + +class DevServerPlugin { + /** + * @param {?Object} options - Dev-Server options + */ + constructor(options) { + this.options = options; + } + + /** + * A Entry, it can be of type string or string[] or Object + * @typedef {(string[] | string | Object)} Entry + */ + + /** + * Apply the plugin + * @param {Object} compiler the compiler instance + * @returns {void} + */ + apply(compiler) { + const { options } = this; + + // 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. + /** @type {string} */ + const domain = createDomain(options, { + address() { + return { port: options.port }; + }, + }); + /** @type {string} */ + const host = + options.client && options.client.host + ? `&host=${options.client.host}` + : ''; + /** @type {string} */ + let path = ''; + // only add the path if it is not default + if ( + options.client && + options.client.path && + options.client.path !== '/ws' + ) { + path = `&path=${options.client.path}`; + } + /** @type {string} */ + const port = + options.client && options.client.port + ? `&port=${options.client.port}` + : ''; + /** @type {string} */ + const clientEntry = `${require.resolve( + '../../client/default/' + )}?${domain}${host}${path}${port}`; + + /** @type {(string[] | string)} */ + let hotEntry; + + if (options.hot === 'only') { + hotEntry = require.resolve('webpack/hot/only-dev-server'); + } else if (options.hot) { + hotEntry = require.resolve('webpack/hot/dev-server'); + } + /** + * prependEntry Method for webpack 4 + * @param {Entry} originalEntry + * @param {Entry} additionalEntries + * @returns {Entry} + */ + const prependEntry = (originalEntry, additionalEntries) => { + if (typeof originalEntry === 'function') { + return () => + Promise.resolve(originalEntry()).then((entry) => + prependEntry(entry, additionalEntries) + ); + } + + if (typeof originalEntry === 'object' && !Array.isArray(originalEntry)) { + /** @type {Object} */ + const clone = {}; + + Object.keys(originalEntry).forEach((key) => { + // entry[key] should be a string here + const entryDescription = originalEntry[key]; + clone[key] = prependEntry(entryDescription, additionalEntries); + }); + + return clone; + } + + // 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); + [].concat(originalEntry).forEach((newEntry) => { + if (!entriesClone.includes(newEntry)) { + entriesClone.push(newEntry); + } + }); + return entriesClone; + }; + + /** + * + * Description of the option for checkInject method + * @typedef {Function} checkInjectOptionsParam + * @param {Object} _config - compilerConfig + * @return {Boolean} + */ + + /** + * + * @param {Boolean | checkInjectOptionsParam} option - inject(Hot|Client) it is Boolean | fn => Boolean + * @param {Object} _config + * @param {Boolean} defaultValue + * @return {Boolean} + */ + // eslint-disable-next-line no-shadow + const checkInject = (option, _config, defaultValue) => { + if (typeof option === 'boolean') return option; + if (typeof option === 'function') return option(_config); + return defaultValue; + }; + + const compilerOptions = compiler.options; + compilerOptions.plugins = compilerOptions.plugins || []; + /** @type {Boolean} */ + const isWebTarget = compilerOptions.externalsPresets + ? compilerOptions.externalsPresets.web + : [ + 'web', + 'webworker', + 'electron-renderer', + 'node-webkit', + // eslint-disable-next-line no-undefined + undefined, + null, + ].includes(compilerOptions.target); + /** @type {Entry} */ + const additionalEntries = checkInject( + options.injectClient, + compilerOptions, + isWebTarget + ) + ? [clientEntry] + : []; + + if (hotEntry && checkInject(options.injectHot, compilerOptions, true)) { + additionalEntries.push(hotEntry); + } + + // 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(); + } + ); + }) + ) + ) + ); + } else { + compilerOptions.entry = prependEntry( + compilerOptions.entry || './src', + additionalEntries + ); + compiler.hooks.entryOption.call( + compilerOptions.context, + compilerOptions.entry + ); + } + + const providePlugin = new webpack.ProvidePlugin({ + __webpack_dev_server_client__: getSocketClientPath(options), + }); + providePlugin.apply(compiler); + + if ( + hotEntry && + !compilerOptions.plugins.find( + (p) => p.constructor === webpack.HotModuleReplacementPlugin + ) + ) { + // apply the HMR plugin, if it didn't exist before. + const plugin = new webpack.HotModuleReplacementPlugin(); + plugin.apply(compiler); + } + } +} + +module.exports = DevServerPlugin; diff --git a/lib/utils/addEntries.js b/lib/utils/addEntries.js deleted file mode 100644 index c04cee5177..0000000000 --- a/lib/utils/addEntries.js +++ /dev/null @@ -1,185 +0,0 @@ -'use strict'; - -const webpack = require('webpack'); -const createDomain = require('./createDomain'); -const DevServerEntryPlugin = require('./DevServerEntryPlugin'); - -/** - * A Entry, it can be of type string or string[] or Object - * @typedef {(string[] | string | Object)} Entry - */ - -/** - * Add entries Method - * @param {?Object} compiler - Webpack compiler - * @param {?Object} options - Dev-Server options - * @param {?Object} server - * @returns {void} - */ -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. - - const app = server || { - address() { - return { port: options.port }; - }, - }; - - /** @type {string} */ - const domain = createDomain(options, app); - /** @type {string} */ - const host = - options.client && options.client.host ? `&host=${options.client.host}` : ''; - /** @type {string} */ - let path = ''; - // only add the path if it is not default - if (options.client && options.client.path && options.client.path !== '/ws') { - path = `&path=${options.client.path}`; - } - /** @type {string} */ - const port = - options.client && options.client.port ? `&port=${options.client.port}` : ''; - /** @type {string} */ - const clientEntry = `${require.resolve( - '../../client/default/' - )}?${domain}${host}${path}${port}`; - - /** @type {(string[] | string)} */ - let hotEntry; - - if (options.hot === 'only') { - hotEntry = require.resolve('webpack/hot/only-dev-server'); - } else if (options.hot) { - hotEntry = require.resolve('webpack/hot/dev-server'); - } - /** - * prependEntry Method for webpack 4 - * @param {Entry} originalEntry - * @param {Entry} additionalEntries - * @returns {Entry} - */ - const prependEntry = (originalEntry, additionalEntries) => { - if (typeof originalEntry === 'function') { - return () => - Promise.resolve(originalEntry()).then((entry) => - prependEntry(entry, additionalEntries) - ); - } - - if (typeof originalEntry === 'object' && !Array.isArray(originalEntry)) { - /** @type {Object} */ - const clone = {}; - - Object.keys(originalEntry).forEach((key) => { - // entry[key] should be a string here - const entryDescription = originalEntry[key]; - clone[key] = prependEntry(entryDescription, additionalEntries); - }); - - return clone; - } - - // 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); - [].concat(originalEntry).forEach((newEntry) => { - if (!entriesClone.includes(newEntry)) { - entriesClone.push(newEntry); - } - }); - return entriesClone; - }; - - /** - * - * Description of the option for checkInject method - * @typedef {Function} checkInjectOptionsParam - * @param {Object} _config - compilerConfig - * @return {Boolean} - */ - - /** - * - * @param {Boolean | checkInjectOptionsParam} option - inject(Hot|Client) it is Boolean | fn => Boolean - * @param {Object} _config - * @param {Boolean} defaultValue - * @return {Boolean} - */ - // eslint-disable-next-line no-shadow - const checkInject = (option, _config, defaultValue) => { - if (typeof option === 'boolean') return option; - if (typeof option === 'function') return option(_config); - return defaultValue; - }; - - const compilers = compiler.compilers || [compiler]; - - // eslint-disable-next-line no-shadow - compilers.forEach((compiler) => { - const config = compiler.options; - /** @type {Boolean} */ - const webTarget = [ - 'web', - 'webworker', - 'electron-renderer', - 'node-webkit', - // eslint-disable-next-line no-undefined - undefined, - null, - ].includes(config.target); - /** @type {Entry} */ - const additionalEntries = checkInject( - options.injectClient, - config, - webTarget - ) - ? [clientEntry] - : []; - - if (hotEntry && checkInject(options.injectHot, config, true)) { - additionalEntries.push(hotEntry); - } - - // 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 || []; - if ( - !config.plugins.find( - // Check for the name rather than the constructor reference in case - // there are multiple copies of webpack installed - (plugin) => plugin.constructor.name === 'HotModuleReplacementPlugin' - ) - ) { - config.plugins.push(new webpack.HotModuleReplacementPlugin()); - } - } - }); -} - -module.exports = addEntries; diff --git a/lib/utils/getSocketClientPath.d.ts b/lib/utils/getSocketClientPath.d.ts new file mode 100644 index 0000000000..e41e415152 --- /dev/null +++ b/lib/utils/getSocketClientPath.d.ts @@ -0,0 +1,3 @@ +declare function getSocketClientPath(options: Object): string; + +export = getSocketClientPath; diff --git a/lib/utils/updateCompiler.js b/lib/utils/updateCompiler.js index ca3d9397d3..a2b4e8f712 100644 --- a/lib/utils/updateCompiler.js +++ b/lib/utils/updateCompiler.js @@ -1,64 +1,13 @@ 'use strict'; -const webpack = require('webpack'); -const addEntries = require('./addEntries'); -const getSocketClientPath = require('./getSocketClientPath'); +const DevServerPlugin = require('./DevServerPlugin'); function updateCompiler(compiler, options) { - const findHMRPlugin = (config) => { - if (!config.plugins) { - // eslint-disable-next-line no-undefined - return undefined; - } - - return config.plugins.find( - (plugin) => plugin.constructor === webpack.HotModuleReplacementPlugin - ); - }; - - const compilers = []; - const compilersWithoutHMR = []; - if (compiler.compilers) { - // eslint-disable-next-line no-shadow - compiler.compilers.forEach((compiler) => { - compilers.push(compiler); - if (!findHMRPlugin(compiler.options)) { - compilersWithoutHMR.push(compiler); - } - }); - } else { - compilers.push(compiler); - if (!findHMRPlugin(compiler.options)) { - compilersWithoutHMR.push(compiler); - } - } - - // it's possible that we should clone the config before doing - // this, but it seems safe not to since it actually reflects - // the changes we are making to the compiler - // important: this relies on the fact that addEntries now - // prevents duplicate new entries. - addEntries(compiler, options); + const compilers = compiler.compilers || [compiler]; // eslint-disable-next-line no-shadow compilers.forEach((compiler) => { - const providePlugin = new webpack.ProvidePlugin({ - __webpack_dev_server_client__: getSocketClientPath(options), - }); - providePlugin.apply(compiler); + new DevServerPlugin(options).apply(compiler); }); - - // do not apply the plugin unless it didn't exist before. - if (options.hot === true || options.hot === 'only') { - // eslint-disable-next-line no-shadow - compilersWithoutHMR.forEach((compiler) => { - // addDevServerEntrypoints above should have added the plugin - // to the compiler options - const plugin = findHMRPlugin(compiler.options); - if (plugin) { - plugin.apply(compiler); - } - }); - } } module.exports = updateCompiler; diff --git a/test/server/Server.test.js b/test/server/Server.test.js index f894290e4c..08bc8c976f 100644 --- a/test/server/Server.test.js +++ b/test/server/Server.test.js @@ -4,7 +4,6 @@ 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,7 +24,7 @@ describe('Server', () => { }); }); - describe('addEntries', () => { + describe('DevServerPlugin', () => { let entries; function getEntries(server) { @@ -58,12 +57,6 @@ describe('Server', () => { getEntries(server); - const plugins = server.middleware.context.compiler.options.plugins; - expect(plugins).toContainEqual(new webpack.HotModuleReplacementPlugin()); - if (isWebpack5) { - expect(plugins[0]).toBeInstanceOf(DevServerEntryPlugin); - } - compiler.hooks.done.tap('webpack-dev-server', () => { expect(entries).toMatchSnapshot(); server.close(done); @@ -83,12 +76,6 @@ describe('Server', () => { getEntries(server); - const plugins = server.middleware.context.compiler.options.plugins; - expect(plugins).toContainEqual(new webpack.HotModuleReplacementPlugin()); - if (isWebpack5) { - expect(plugins[0]).toBeInstanceOf(DevServerEntryPlugin); - } - compiler.hooks.done.tap('webpack-dev-server', () => { expect(entries).toMatchSnapshot(); server.close(done); diff --git a/test/server/__snapshots__/Server.test.js.snap b/test/server/__snapshots__/Server.test.js.snap index 956a845c89..d7cf97eb62 100644 --- a/test/server/__snapshots__/Server.test.js.snap +++ b/test/server/__snapshots__/Server.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Server addEntries add hot option 1`] = ` +exports[`Server DevServerPlugin add hot option 1`] = ` Array [ Array [ "client", @@ -20,7 +20,7 @@ Array [ ] `; -exports[`Server addEntries add hot-only option 1`] = ` +exports[`Server DevServerPlugin add hot-only option 1`] = ` Array [ Array [ "client", diff --git a/test/server/utils/DevServerEntryPlugin.test.js b/test/server/utils/DevServerEntryPlugin.test.js deleted file mode 100644 index df4c3d4283..0000000000 --- a/test/server/utils/DevServerEntryPlugin.test.js +++ /dev/null @@ -1,72 +0,0 @@ -'use strict'; - -const isWebpack5 = require('../../helpers/isWebpack5'); - -(isWebpack5 ? describe : describe.skip)('DevServerEntryPlugin', () => { - let plugin; - const options = {}; - const entries = ['./foo.js', './bar.js']; - - const createDependency = jest.fn(); - const tapPromise = jest.fn(); - const compiler = { - hooks: { - make: { - tapPromise, - }, - }, - }; - const compilation = { - addEntry: jest.fn((_context, _dep, _options, cb) => cb()), - }; - - beforeEach(() => { - jest.setMock('webpack/lib/EntryPlugin.js', { createDependency }); - const DevServerEntryPlugin = require('../../../lib/utils/DevServerEntryPlugin'); - plugin = new DevServerEntryPlugin('context', entries, options); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should set property', () => { - expect(plugin.context).toBe('context'); - expect(plugin.entries).toBe(entries); - expect(plugin.options).toBe(options); - }); - - it('should add hooks to add entries', async () => { - plugin.apply(compiler); - expect(tapPromise).toBeCalledTimes(1); - expect(tapPromise.mock.calls[0]).toMatchSnapshot(); - - await tapPromise.mock.calls[0][1](compilation); - expect(compilation.addEntry).toBeCalledTimes(entries.length); - expect(compilation.addEntry.mock.calls).toMatchSnapshot(); - - expect(createDependency).toBeCalledTimes(entries.length); - expect(createDependency.mock.calls).toMatchSnapshot(); - }); - - it('should allow modifying entries after creation', async () => { - plugin.apply(compiler); - jest.clearAllMocks(); - - const newEntries = ['./foobar.js']; - plugin.entries = newEntries; - expect(plugin.entries).toBe(newEntries); - - plugin.apply(compiler); - - expect(tapPromise).toBeCalledTimes(1); - expect(tapPromise.mock.calls[0]).toMatchSnapshot(); - - await tapPromise.mock.calls[0][1](compilation); - expect(compilation.addEntry).toBeCalledTimes(newEntries.length); - expect(compilation.addEntry.mock.calls).toMatchSnapshot(); - - expect(createDependency).toBeCalledTimes(newEntries.length); - expect(createDependency.mock.calls).toMatchSnapshot(); - }); -}); diff --git a/test/server/utils/DevServerPlugin.test.js b/test/server/utils/DevServerPlugin.test.js new file mode 100644 index 0000000000..b7de5cc00b --- /dev/null +++ b/test/server/utils/DevServerPlugin.test.js @@ -0,0 +1,764 @@ +'use strict'; + +const path = require('path'); +const webpack = require('webpack'); +const DevServerPlugin = require('../../../lib/utils/DevServerPlugin'); +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'); + +const normalize = (entry) => entry.split(path.sep).join('/'); + +describe('DevServerPlugin util', () => { + async function getEntries(compiler) { + const entryOption = compiler.options.entry; + if (isWebpack5) { + const entries = []; + + const compilation = { + addEntry(_context, dep, _options, cb) { + if (!dep.loc.name) { + entries.push(dep.request); + } + cb(); + }, + }; + await Promise.all( + compiler.hooks.make.taps + .filter((tap) => tap.name === 'DevServerPlugin') + .map((tap) => tap.fn(compilation)) + ); + + // 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; + } + + (isWebpack5 ? it : it.skip)('should add hooks to add entries', async () => { + const tapPromise = jest.fn(); + const compiler = { + options: {}, + hooks: { + compilation: { + tap: jest.fn(), + }, + make: { + tapPromise, + }, + }, + }; + const devServerOptions = { + transportMode: { + server: 'sockjs', + client: 'sockjs', + }, + }; + const compilation = { + addEntry: jest.fn((_context, _dep, _options, cb) => cb()), + }; + + const plugin = new DevServerPlugin(devServerOptions); + plugin.apply(compiler); + expect(tapPromise).toBeCalledTimes(1); + expect(tapPromise.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "DevServerPlugin", + [Function], + ] + `); + + await tapPromise.mock.calls[0][1](compilation); + expect(compilation.addEntry).toBeCalledTimes(1); + }); + + it('should adds devServer entry points to a single entry point', async () => { + const webpackOptions = Object.assign({}, config); + const compiler = webpack(webpackOptions); + const devServerOptions = { + transportMode: { + server: 'sockjs', + client: 'sockjs', + }, + }; + + const plugin = new DevServerPlugin(devServerOptions); + plugin.apply(compiler); + const entries = await getEntries(compiler); + + 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 adds devServer entry points to a multi-module entry point', async () => { + const webpackOptions = Object.assign({}, config, { + entry: ['./foo.js', './bar.js'], + }); + const compiler = webpack(webpackOptions); + + const devServerOptions = { + transportMode: { + server: 'sockjs', + client: 'sockjs', + }, + }; + + const plugin = new DevServerPlugin(devServerOptions); + plugin.apply(compiler); + const entries = await getEntries(compiler); + + expect(entries.length).toEqual(3); + expect( + normalize(entries[0]).indexOf('client/default/index.js?') !== -1 + ).toBeTruthy(); + expect(entries[1]).toEqual('./foo.js'); + expect(entries[2]).toEqual('./bar.js'); + }); + + it('should adds devServer entry points to a multi entry point object', async () => { + const webpackOptions = Object.assign({}, config, { + entry: { + foo: './foo.js', + bar: './bar.js', + }, + }); + const compiler = webpack(webpackOptions); + + const devServerOptions = { + transportMode: { + server: 'sockjs', + client: 'sockjs', + }, + }; + + const plugin = new DevServerPlugin(devServerOptions); + plugin.apply(compiler); + const entries = await getEntries(compiler); + + if (isWebpack5) { + expect(entries.length).toEqual(1); + expect( + normalize(entries[0]).indexOf('client/default/index.js?') !== -1 + ).toBeTruthy(); + + 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(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', async () => { + const webpackOptions = {}; + const compiler = webpack(webpackOptions); + const devServerOptions = { + transportMode: { + server: 'sockjs', + client: 'sockjs', + }, + }; + + const plugin = new DevServerPlugin(devServerOptions); + plugin.apply(compiler); + const entries = await getEntries(compiler); + + expect(entries.length).toEqual(2); + expect(entries[1]).toEqual('./src'); + }); + + it('should preserves dynamic entry points', (done) => { + let i = 0; + const webpackOptions = { + // simulate dynamic entry + entry: () => { + i += 1; + return `./src-${i}.js`; + }, + }; + const compiler = webpack(webpackOptions); + const devServerOptions = { + transportMode: { + server: 'sockjs', + client: 'sockjs', + }, + }; + + const plugin = new DevServerPlugin(devServerOptions); + plugin.apply(compiler); + getEntries(compiler).then((entries) => { + expect(typeof entries).toEqual('function'); + + entries() + .then((entryFirstRun) => + 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(); + }) + ) + .catch(done); + }); + }); + + it('should preserves asynchronous dynamic entry points', (done) => { + let i = 0; + const webpackOptions = { + // simulate async dynamic entry + entry: () => + new Promise((resolve) => { + i += 1; + resolve(`./src-${i}.js`); + }), + }; + const compiler = webpack(webpackOptions); + + const devServerOptions = { + transportMode: { + server: 'sockjs', + client: 'sockjs', + }, + }; + + const plugin = new DevServerPlugin(devServerOptions); + plugin.apply(compiler); + getEntries(compiler).then((entries) => { + expect(typeof entries).toEqual('function'); + + entries() + .then((entryFirstRun) => + 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(); + }) + ) + .catch(done); + }); + }); + + it("should prepends webpack's hot reload client script", async () => { + const webpackOptions = Object.assign({}, config, { + entry: { + app: './app.js', + }, + }); + const compiler = webpack(webpackOptions); + + const devServerOptions = { + hot: true, + transportMode: { + server: 'sockjs', + client: 'sockjs', + }, + }; + + const plugin = new DevServerPlugin(devServerOptions); + plugin.apply(compiler); + const entries = await getEntries(compiler); + + const hotClientScript = (isWebpack5 ? entries : entries.app)[1]; + + expect( + normalize(hotClientScript).includes('webpack/hot/dev-server') + ).toBeTruthy(); + expect(hotClientScript).toEqual(require.resolve(hotClientScript)); + }); + + it("should prepends webpack's hot-only client script", async () => { + const webpackOptions = Object.assign({}, config, { + entry: { + app: './app.js', + }, + }); + const compiler = webpack(webpackOptions); + + const devServerOptions = { + hot: 'only', + transportMode: { + server: 'sockjs', + client: 'sockjs', + }, + }; + + const plugin = new DevServerPlugin(devServerOptions); + plugin.apply(compiler); + const entries = await getEntries(compiler); + + const hotClientScript = (isWebpack5 ? entries : entries.app)[1]; + + expect( + normalize(hotClientScript).includes('webpack/hot/only-dev-server') + ).toBeTruthy(); + expect(hotClientScript).toEqual(require.resolve(hotClientScript)); + }); + + 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 = { + transportMode: { + server: 'sockjs', + client: 'sockjs', + }, + }; + + const plugin = new DevServerPlugin(devServerOptions); + plugin.apply(compiler); + + 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 = { + transportMode: { + server: 'sockjs', + client: 'sockjs', + }, + }; + + const plugin = new DevServerPlugin(devServerOptions); + plugin.apply(compiler); + + expect(webpackOptions.plugins).toEqual([]); + }); + + it("should doesn't add the HMR plugin if not hot and some plugins", () => { + const existingPlugin1 = new webpack.BannerPlugin('happy birthday'); + const existingPlugin2 = new webpack.DefinePlugin({ foo: 'bar' }); + const webpackOptions = Object.assign({}, config, { + plugins: [existingPlugin1, existingPlugin2], + }); + const compiler = webpack(webpackOptions); + const devServerOptions = { + transportMode: { + server: 'sockjs', + client: 'sockjs', + }, + }; + + const plugin = new DevServerPlugin(devServerOptions); + plugin.apply(compiler); + + expect(webpackOptions.plugins).toEqual([existingPlugin1, existingPlugin2]); + }); + + it("should doesn't add the HMR plugin again if it's already there", () => { + const existingPlugin = new webpack.BannerPlugin('bruce'); + const webpackOptions = Object.assign({}, config, { + plugins: [new webpack.HotModuleReplacementPlugin(), existingPlugin], + }); + const compiler = webpack(webpackOptions); + const devServerOptions = { + hot: true, + transportMode: { + server: 'sockjs', + client: 'sockjs', + }, + }; + + const plugin = new DevServerPlugin(devServerOptions); + plugin.apply(compiler); + + expect(webpackOptions.plugins).toEqual([ + new webpack.HotModuleReplacementPlugin(), + existingPlugin, + ]); + }); + + (isWebpack5 ? it.skip : it)( + 'should can prevent duplicate entries from successive calls', + async () => { + const webpackOptions = Object.assign({}, config); + const compiler = webpack(webpackOptions); + const devServerOptions = { + hot: true, + transportMode: { + server: 'sockjs', + client: 'sockjs', + }, + }; + + const plugin = new DevServerPlugin(devServerOptions); + plugin.apply(compiler); + plugin.apply(compiler); + const entries = await getEntries(compiler); + + expect(entries.length).toEqual(3); + + const result = entries.filter((entry) => + normalize(entry).includes('webpack/hot/dev-server') + ); + expect(result.length).toEqual(1); + } + ); + + it('should supports entry as Function', async () => { + const webpackOptions = Object.assign({}, configEntryAsFunction); + const compiler = webpack(webpackOptions); + const devServerOptions = { + transportMode: { + server: 'sockjs', + client: 'sockjs', + }, + }; + + const plugin = new DevServerPlugin(devServerOptions); + plugin.apply(compiler); + const entries = await getEntries(compiler); + + expect(typeof entries === 'function').toBe(true); + }); + + (isWebpack5 ? it : it.skip)( + 'should supports entry as descriptor', + async () => { + const webpackOptions = Object.assign({}, configEntryAsDescriptor); + const compiler = webpack(webpackOptions); + const devServerOptions = { + transportMode: { + server: 'sockjs', + client: 'sockjs', + }, + }; + + const plugin = new DevServerPlugin(devServerOptions); + plugin.apply(compiler); + const entries = await getEntries(compiler); + + 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', async () => { + const webpackOptions = [ + Object.assign({}, config), + Object.assign({ target: 'web' }, config), + Object.assign({ target: 'webworker' }, config), + Object.assign({ target: 'electron-renderer' }, config), + Object.assign({ target: 'node-webkit' }, config), + Object.assign({ target: 'node' }, config) /* index:5 */, + ]; + const compiler = webpack(webpackOptions); + + const devServerOptions = { + transportMode: { + server: 'sockjs', + client: 'sockjs', + }, + }; + + await Promise.all( + // eslint-disable-next-line no-shadow + compiler.compilers.map((compiler, index) => { + const plugin = new DevServerPlugin(devServerOptions); + plugin.apply(compiler); + + return getEntries(compiler).then((entries) => { + const expectInline = index !== 5; /* all but the node target */ + + expect(entries.length).toEqual(expectInline ? 2 : 1); + + if (expectInline) { + expect( + normalize(entries[0]).indexOf('client/default/index.js?') !== -1 + ).toBeTruthy(); + } + + expect(normalize(entries[expectInline ? 1 : 0])).toEqual('./foo.js'); + }); + }) + ); + }); + + (isWebpack5 ? it : it.skip)( + 'should prepend devServer entry points depending on targetProperties', + async () => { + // https://github.com/webpack/webpack/issues/11660 + const configNoChunkLoading = Object.assign({}, config); + configNoChunkLoading.output = Object.assign( + { + chunkLoading: false, + wasmLoading: false, + workerChunkLoading: false, + }, + config.output + ); + + const webpackOptions = [ + Object.assign({ target: ['web', 'webworker'] }, configNoChunkLoading), + Object.assign({ target: 'browserslist:last 2 versions' }, config), + Object.assign({ target: ['web', 'node'] }, configNoChunkLoading), + Object.assign( + { target: 'browserslist:last 2 versions, maintained node versions' }, + configNoChunkLoading + ), + Object.assign( + { target: 'browserslist:maintained node versions' }, + config + ), + Object.assign({ target: false }, config), + ]; + const compiler = webpack(webpackOptions); + + const devServerOptions = { + transportMode: { + server: 'sockjs', + client: 'sockjs', + }, + }; + + await Promise.all( + // eslint-disable-next-line no-shadow + compiler.compilers.map((compiler, index) => { + const plugin = new DevServerPlugin(devServerOptions); + plugin.apply(compiler); + + return getEntries(compiler).then((entries) => { + const expectInline = index < 2; /* all but the node target */ + + expect(entries.length).toEqual(expectInline ? 2 : 1); + + if (expectInline) { + expect( + normalize(entries[0]).indexOf('client/default/index.js?') !== -1 + ).toBeTruthy(); + } + + expect(normalize(entries[expectInline ? 1 : 0])).toEqual( + './foo.js' + ); + }); + }) + ); + } + ); + + it('should allows selecting compilations to inline the client into', async () => { + const webpackOptions = [ + Object.assign({}, config), + Object.assign({ target: 'web' }, config), + 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', + transportMode: { + server: 'sockjs', + client: 'sockjs', + }, + }; + + await Promise.all( + // eslint-disable-next-line no-shadow + compiler.compilers.map((compiler, index) => { + const plugin = new DevServerPlugin(devServerOptions); + plugin.apply(compiler); + return getEntries(compiler).then((entries) => { + const expectInline = + index === 2; /* only the "only-include" compiler */ + + expect(entries.length).toEqual(expectInline ? 2 : 1); + + if (expectInline) { + expect( + normalize(entries[0]).indexOf('client/default/index.js?') !== -1 + ).toBeTruthy(); + } + + expect(normalize(entries[expectInline ? 1 : 0])).toEqual('./foo.js'); + }); + }) + ); + }); + + it('should prepends the hot runtime to all targets by default (when hot)', async () => { + const webpackOptions = [ + 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 + // and we can use the same assertions for both configs + injectClient: false, + hot: true, + 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(entries.length).toEqual(2); + + expect( + normalize(entries[0]).includes('webpack/hot/dev-server') + ).toBeTruthy(); + + expect(normalize(entries[1])).toEqual('./foo.js'); + }); + }) + ); + }); + + it('should allows selecting which compilations to inject the hot runtime into', async () => { + const webpackOptions = [ + Object.assign({ target: 'web' }, config), + Object.assign({ target: 'node' }, config), + ]; + const compiler = webpack(webpackOptions); + + const devServerOptions = { + injectHot: (compilerConfig) => compilerConfig.target === 'node', + hot: true, + transportMode: { + server: 'sockjs', + client: 'sockjs', + }, + }; + + // eslint-disable-next-line no-shadow + compiler.compilers.forEach((compiler) => { + const plugin = new DevServerPlugin(devServerOptions); + plugin.apply(compiler); + }); + + // node target should have the client runtime but not the hot runtime + const webEntries = await getEntries(compiler.compilers[0]); + + expect(webEntries.length).toEqual(2); + + expect( + normalize(webEntries[0]).indexOf('client/default/index.js?') !== -1 + ).toBeTruthy(); + + expect(normalize(webEntries[1])).toEqual('./foo.js'); + + // node target should have the hot runtime but not the client runtime + const nodeEntries = await getEntries(compiler.compilers[1]); + + expect(nodeEntries.length).toEqual(2); + + expect( + normalize(nodeEntries[0]).includes('webpack/hot/dev-server') + ).toBeTruthy(); + + expect(normalize(nodeEntries[1])).toEqual('./foo.js'); + }); + + it('does not use client.path when default', async () => { + const webpackOptions = Object.assign({}, config); + const compiler = webpack(webpackOptions); + const devServerOptions = { + client: { + path: '/ws', + }, + transportMode: { + server: 'sockjs', + client: 'sockjs', + }, + }; + + const plugin = new DevServerPlugin(devServerOptions); + plugin.apply(compiler); + const entries = await getEntries(compiler); + expect(entries[0]).not.toContain('&path=/ws'); + }); + + it('uses custom client.path', async () => { + const webpackOptions = Object.assign({}, config); + const compiler = webpack(webpackOptions); + const devServerOptions = { + client: { + path: '/custom/path', + }, + transportMode: { + server: 'sockjs', + client: 'sockjs', + }, + }; + + const plugin = new DevServerPlugin(devServerOptions); + plugin.apply(compiler); + const entries = await getEntries(compiler); + expect(entries[0]).toContain('&path=/custom/path'); + }); + + it('uses custom client', async () => { + const webpackOptions = Object.assign({}, config); + const compiler = webpack(webpackOptions); + const devServerOptions = { + client: { + host: 'my.host', + port: 8080, + path: '/custom/path', + }, + transportMode: { + server: 'sockjs', + client: 'sockjs', + }, + }; + + const plugin = new DevServerPlugin(devServerOptions); + plugin.apply(compiler); + const entries = await getEntries(compiler); + expect(entries[0]).toContain('&host=my.host&path=/custom/path&port=8080'); + }); +}); diff --git a/test/server/utils/__snapshots__/DevServerEntryPlugin.test.js.snap b/test/server/utils/__snapshots__/DevServerEntryPlugin.test.js.snap deleted file mode 100644 index a5e909274b..0000000000 --- a/test/server/utils/__snapshots__/DevServerEntryPlugin.test.js.snap +++ /dev/null @@ -1,65 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DevServerEntryPlugin should add hooks to add entries 1`] = ` -Array [ - "DevServerEntryPlugin", - [Function], -] -`; - -exports[`DevServerEntryPlugin should add hooks to add entries 2`] = ` -Array [ - Array [ - "context", - undefined, - Object {}, - [Function], - ], - Array [ - "context", - undefined, - Object {}, - [Function], - ], -] -`; - -exports[`DevServerEntryPlugin should add hooks to add entries 3`] = ` -Array [ - Array [ - "./foo.js", - Object {}, - ], - Array [ - "./bar.js", - Object {}, - ], -] -`; - -exports[`DevServerEntryPlugin should allow modifying entries after creation 1`] = ` -Array [ - "DevServerEntryPlugin", - [Function], -] -`; - -exports[`DevServerEntryPlugin should allow modifying entries after creation 2`] = ` -Array [ - Array [ - "context", - undefined, - Object {}, - [Function], - ], -] -`; - -exports[`DevServerEntryPlugin should allow modifying entries after creation 3`] = ` -Array [ - Array [ - "./foobar.js", - Object {}, - ], -] -`; diff --git a/test/server/utils/addEntries.test.js b/test/server/utils/addEntries.test.js deleted file mode 100644 index 41ad67aec1..0000000000 --- a/test/server/utils/addEntries.test.js +++ /dev/null @@ -1,564 +0,0 @@ -'use strict'; - -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'); - -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(compiler, devServerOptions); - const entries = getEntries(compiler); - - 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 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(compiler, devServerOptions); - const entries = getEntries(compiler); - - expect(entries.length).toEqual(3); - expect( - normalize(entries[0]).indexOf('client/default/index.js?') !== -1 - ).toBeTruthy(); - expect(entries[1]).toEqual('./foo.js'); - expect(entries[2]).toEqual('./bar.js'); - }); - - it('should adds devServer entry points to a multi entry point object', () => { - const webpackOptions = Object.assign({}, config, { - entry: { - foo: './foo.js', - bar: './bar.js', - }, - }); - const compiler = webpack(webpackOptions); - - const 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(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(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(compiler, devServerOptions); - const entries = getEntries(compiler); - - expect(entries.length).toEqual(2); - expect(entries[1]).toEqual('./src'); - }); - - it('should preserves dynamic entry points', (done) => { - let i = 0; - const webpackOptions = { - // simulate dynamic entry - entry: () => { - i += 1; - return `./src-${i}.js`; - }, - }; - const compiler = webpack(webpackOptions); - const devServerOptions = {}; - - addEntries(compiler, devServerOptions); - const entries = getEntries(compiler); - - expect(typeof entries).toEqual('function'); - - entries() - .then((entryFirstRun) => - 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(); - }) - ) - .catch(done); - }); - - it('should preserves asynchronous dynamic entry points', (done) => { - let i = 0; - const webpackOptions = { - // simulate async dynamic entry - entry: () => - new Promise((resolve) => { - i += 1; - resolve(`./src-${i}.js`); - }), - }; - const compiler = webpack(webpackOptions); - - const devServerOptions = {}; - - addEntries(compiler, devServerOptions); - const entries = getEntries(compiler); - - expect(typeof entries).toEqual('function'); - - entries() - .then((entryFirstRun) => - 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(); - }) - ) - .catch(done); - }); - - it("should prepends webpack's hot reload client script", () => { - const webpackOptions = Object.assign({}, config, { - entry: { - app: './app.js', - }, - }); - const compiler = webpack(webpackOptions); - - const devServerOptions = { - hot: true, - }; - - addEntries(compiler, devServerOptions); - const entries = getEntries(compiler); - - const hotClientScript = (isWebpack5 ? entries : entries.app)[1]; - - expect( - normalize(hotClientScript).includes('webpack/hot/dev-server') - ).toBeTruthy(); - expect(hotClientScript).toEqual(require.resolve(hotClientScript)); - }); - - it("should prepends webpack's hot-only client script", () => { - const webpackOptions = Object.assign({}, config, { - entry: { - app: './app.js', - }, - }); - const compiler = webpack(webpackOptions); - - const devServerOptions = { - hot: 'only', - }; - - addEntries(compiler, devServerOptions); - const entries = getEntries(compiler); - - const hotClientScript = (isWebpack5 ? entries : entries.app)[1]; - - expect( - normalize(hotClientScript).includes('webpack/hot/only-dev-server') - ).toBeTruthy(); - expect(hotClientScript).toEqual(require.resolve(hotClientScript)); - }); - - 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(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(compiler, devServerOptions); - - expect(webpackOptions.plugins).toEqual([]); - }); - - it("should doesn't add the HMR plugin if not hot and some plugins", () => { - const existingPlugin1 = new webpack.BannerPlugin('happy birthday'); - const existingPlugin2 = new webpack.DefinePlugin({ foo: 'bar' }); - const webpackOptions = Object.assign({}, config, { - plugins: [existingPlugin1, existingPlugin2], - }); - const compiler = webpack(webpackOptions); - const devServerOptions = {}; - - addEntries(compiler, devServerOptions); - - expect(webpackOptions.plugins).toEqual([existingPlugin1, existingPlugin2]); - }); - - it('should adds the HMR plugin if hot', () => { - const existingPlugin = new webpack.BannerPlugin('bruce'); - const webpackOptions = Object.assign({}, config, { - plugins: [existingPlugin], - }); - const compiler = webpack(webpackOptions); - const devServerOptions = { hot: true }; - - addEntries(compiler, devServerOptions); - - 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(compiler, devServerOptions); - - expect(compiler.options.plugins).toContainEqual( - new webpack.HotModuleReplacementPlugin() - ); - }); - - it("should doesn't add the HMR plugin again if it's already there", () => { - const existingPlugin = new webpack.BannerPlugin('bruce'); - const webpackOptions = Object.assign({}, config, { - plugins: [new webpack.HotModuleReplacementPlugin(), existingPlugin], - }); - const compiler = webpack(webpackOptions); - const devServerOptions = { hot: true }; - - addEntries(compiler, devServerOptions); - - expect(webpackOptions.plugins).toEqual([ - new webpack.HotModuleReplacementPlugin(), - existingPlugin, - ]); - }); - - it("should not add the HMR plugin again if it's already there from a different webpack", () => { - const existingPlugin = new webpack.BannerPlugin('bruce'); - - // Simulate the inclusion of another webpack's 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(compiler, devServerOptions); - - expect(webpackOptions.plugins).toEqual([ - // Nothing should be injected - new HotModuleReplacementPlugin(), - existingPlugin, - ]); - }); - - it('should can prevent duplicate entries from successive calls', () => { - const webpackOptions = Object.assign({}, config); - const compiler = webpack(webpackOptions); - const devServerOptions = { hot: true }; - - addEntries(compiler, devServerOptions); - addEntries(compiler, devServerOptions); - const entries = getEntries(compiler); - - expect(entries.length).toEqual(3); - - const result = entries.filter((entry) => - normalize(entry).includes('webpack/hot/dev-server') - ); - expect(result.length).toEqual(1); - }); - - it('should supports entry as Function', () => { - const webpackOptions = Object.assign({}, configEntryAsFunction); - const compiler = webpack(webpackOptions); - const devServerOptions = {}; - - addEntries(compiler, devServerOptions); - const entries = getEntries(compiler); - - expect(typeof entries === 'function').toBe(true); - }); - - (isWebpack5 ? it : it.skip)('should supports entry as descriptor', () => { - const webpackOptions = Object.assign({}, configEntryAsDescriptor); - const compiler = webpack(webpackOptions); - const devServerOptions = {}; - - addEntries(compiler, devServerOptions); - const entries = getEntries(compiler); - - 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', () => { - const webpackOptions = [ - Object.assign({}, config), - Object.assign({ target: 'web' }, config), - Object.assign({ target: 'webworker' }, config), - Object.assign({ target: 'electron-renderer' }, config), - Object.assign({ target: 'node-webkit' }, config), - Object.assign({ target: 'node' }, config) /* index:5 */, - ]; - const compiler = webpack(webpackOptions); - - const devServerOptions = {}; - - addEntries(compiler, devServerOptions); - - // eslint-disable-next-line no-shadow - compiler.compilers.forEach((compiler, index) => { - const entries = getEntries(compiler); - const expectInline = index !== 5; /* all but the node target */ - - expect(entries.length).toEqual(expectInline ? 2 : 1); - - if (expectInline) { - expect( - normalize(entries[0]).indexOf('client/default/index.js?') !== -1 - ).toBeTruthy(); - } - - expect(normalize(entries[expectInline ? 1 : 0])).toEqual('./foo.js'); - }); - }); - - it('should allows selecting compilations to inline the client into', () => { - const webpackOptions = [ - Object.assign({}, config), - Object.assign({ target: 'web' }, config), - 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(compiler, devServerOptions); - - // eslint-disable-next-line no-shadow - compiler.compilers.forEach((compiler, index) => { - const entries = getEntries(compiler); - const expectInline = index === 2; /* only the "only-include" compiler */ - - expect(entries.length).toEqual(expectInline ? 2 : 1); - - if (expectInline) { - expect( - normalize(entries[0]).indexOf('client/default/index.js?') !== -1 - ).toBeTruthy(); - } - - expect(normalize(entries[expectInline ? 1 : 0])).toEqual('./foo.js'); - }); - }); - - it('should prepends the hot runtime to all targets by default (when hot)', () => { - const webpackOptions = [ - 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 - // and we can use the same assertions for both configs - injectClient: false, - hot: true, - }; - - addEntries(compiler, devServerOptions); - - // eslint-disable-next-line no-shadow - compiler.compilers.forEach((compiler) => { - const entries = getEntries(compiler); - expect(entries.length).toEqual(2); - - expect( - normalize(entries[0]).includes('webpack/hot/dev-server') - ).toBeTruthy(); - - expect(normalize(entries[1])).toEqual('./foo.js'); - }); - }); - - it('should allows selecting which compilations to inject the hot runtime into', () => { - const webpackOptions = [ - Object.assign({ target: 'web' }, config), - Object.assign({ target: 'node' }, config), - ]; - const compiler = webpack(webpackOptions); - - const devServerOptions = { - injectHot: (compilerConfig) => compilerConfig.target === 'node', - hot: true, - }; - - addEntries(compiler, devServerOptions); - - // node target should have the client runtime but not the hot runtime - const webEntries = getEntries(compiler.compilers[0]); - - expect(webEntries.length).toEqual(2); - - expect( - normalize(webEntries[0]).indexOf('client/default/index.js?') !== -1 - ).toBeTruthy(); - - expect(normalize(webEntries[1])).toEqual('./foo.js'); - - // node target should have the hot runtime but not the client runtime - const nodeEntries = getEntries(compiler.compilers[1]); - - expect(nodeEntries.length).toEqual(2); - - expect( - normalize(nodeEntries[0]).includes('webpack/hot/dev-server') - ).toBeTruthy(); - - 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(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(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', - port: 8080, - path: '/custom/path', - }, - }; - - 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 8ba3f7b43d..eb3f91fd5f 100644 --- a/test/server/utils/updateCompiler.test.js +++ b/test/server/utils/updateCompiler.test.js @@ -2,14 +2,13 @@ const webpack = require('webpack'); const updateCompiler = require('../../../lib/utils/updateCompiler'); -const DevServerEntryPlugin = require('../../../lib/utils/DevServerEntryPlugin'); const isWebpack5 = require('../../helpers/isWebpack5'); describe('updateCompiler', () => { describe('simple config, no hot', () => { let compiler; - beforeAll(() => { + beforeEach(() => { const webpackConfig = require('../../fixtures/simple-config/webpack.config'); compiler = webpack(webpackConfig); @@ -25,6 +24,7 @@ describe('updateCompiler', () => { let tapsByHMR = 0; let tapsByProvidePlugin = 0; + let tapsByDevServerPlugin = 0; compiler.hooks.compilation.taps.forEach((tap) => { if (tap.name === 'HotModuleReplacementPlugin') { @@ -34,18 +34,17 @@ describe('updateCompiler', () => { } }); + compiler.hooks.make.taps.forEach((tap) => { + if (tap.name === 'DevServerPlugin') { + tapsByDevServerPlugin += 1; + } + }); + expect(compiler.hooks.entryOption.taps.length).toBe(1); expect(tapsByHMR).toEqual(0); expect(tapsByProvidePlugin).toEqual(1); - - if (isWebpack5) { - expect(compiler.options.plugins).toHaveLength(1); - expect(compiler.options.plugins[0]).toBeInstanceOf( - DevServerEntryPlugin - ); - } else { - expect(compiler.options.plugins).toBeUndefined(); - } + expect(tapsByDevServerPlugin).toEqual(isWebpack5 ? 1 : 0); + expect(compiler.options.plugins).toHaveLength(0); }); }); @@ -69,6 +68,7 @@ describe('updateCompiler', () => { let tapsByHMR = 0; let tapsByProvidePlugin = 0; + let tapsByDevServerPlugin = 0; compiler.hooks.compilation.taps.forEach((tap) => { if (tap.name === 'HotModuleReplacementPlugin') { @@ -78,17 +78,16 @@ describe('updateCompiler', () => { } }); + compiler.hooks.make.taps.forEach((tap) => { + if (tap.name === 'DevServerPlugin') { + tapsByDevServerPlugin += 1; + } + }); + expect(compiler.hooks.entryOption.taps.length).toBe(1); expect(tapsByHMR).toEqual(1); expect(tapsByProvidePlugin).toEqual(1); - expect(compiler.options.plugins).toContainEqual( - new webpack.HotModuleReplacementPlugin() - ); - if (isWebpack5) { - expect(compiler.options.plugins[0]).toBeInstanceOf( - DevServerEntryPlugin - ); - } + expect(tapsByDevServerPlugin).toEqual(isWebpack5 ? 1 : 0); }); }); @@ -114,6 +113,7 @@ describe('updateCompiler', () => { let tapsByHMR = 0; let tapsByProvidePlugin = 0; + let tapsByDevServerPlugin = 0; compiler.hooks.compilation.taps.forEach((tap) => { if (tap.name === 'HotModuleReplacementPlugin') { @@ -123,17 +123,19 @@ describe('updateCompiler', () => { } }); + compiler.hooks.make.taps.forEach((tap) => { + if (tap.name === 'DevServerPlugin') { + tapsByDevServerPlugin += 1; + } + }); + expect(compiler.hooks.entryOption.taps.length).toBe(1); expect(tapsByHMR).toEqual(1); expect(tapsByProvidePlugin).toEqual(1); + expect(tapsByDevServerPlugin).toEqual(isWebpack5 ? 1 : 0); expect(compiler.options.plugins).toContainEqual( new webpack.HotModuleReplacementPlugin() ); - if (isWebpack5) { - expect(compiler.options.plugins[1]).toBeInstanceOf( - DevServerEntryPlugin - ); - } }); }); @@ -157,9 +159,10 @@ describe('updateCompiler', () => { hot: true, }); - multiCompiler.compilers.forEach((compiler, index) => { + multiCompiler.compilers.forEach((compiler) => { let tapsByHMR = 0; let tapsByProvidePlugin = 0; + let tapsByDevServerPlugin = 0; compiler.hooks.compilation.taps.forEach((tap) => { if (tap.name === 'HotModuleReplacementPlugin') { @@ -169,17 +172,16 @@ describe('updateCompiler', () => { } }); + compiler.hooks.make.taps.forEach((tap) => { + if (tap.name === 'DevServerPlugin') { + tapsByDevServerPlugin += 1; + } + }); + expect(compiler.hooks.entryOption.taps.length).toBe(1); expect(tapsByHMR).toEqual(1); expect(tapsByProvidePlugin).toEqual(1); - expect(compiler.options.plugins).toContainEqual( - new webpack.HotModuleReplacementPlugin() - ); - if (isWebpack5) { - expect(compiler.options.plugins[index]).toBeInstanceOf( - DevServerEntryPlugin - ); - } + expect(tapsByDevServerPlugin).toEqual(isWebpack5 ? 1 : 0); }); }); }); diff --git a/tsconfig.json b/tsconfig.json index 8e65f3f910..141d025c4d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,5 +12,5 @@ "types": ["node"], "esModuleInterop": true }, - "include": ["lib/utils/addEntries.js"] + "include": ["lib/utils/DevServerPlugin.js"] }