diff --git a/src/services/configurations/browserDevelopmentConfiguration.js b/src/services/configurations/browserDevelopmentConfiguration.js index df7e5ce..72b8629 100644 --- a/src/services/configurations/browserDevelopmentConfiguration.js +++ b/src/services/configurations/browserDevelopmentConfiguration.js @@ -111,16 +111,29 @@ class WebpackBrowserDevelopmentConfiguration extends ConfigurationFile { const hotEntries = []; // If the target needs to run on development... if (target.runOnDevelopment) { + const devServerConfig = this._normalizeTargetDevServerSettings(target); // Add the dev server information to the configuration. - const { devServer } = target; - const devServerHost = devServer.host || 'localhost'; config.devServer = { - port: devServer.port || 2509, - inline: !!devServer.reload, + port: devServerConfig.port, + inline: !!devServerConfig.reload, open: true, + openPage: '/', }; - if (devServerHost !== 'localhost') { - config.devServer.public = devServerHost; + // If the configuration has a custom host, set it. + if (devServerConfig.host !== 'localhost') { + config.devServer.host = devServerConfig.host; + } + // If there are SSL files, set them on the server. + if (devServerConfig.ssl) { + config.devServer.https = { + key: devServerConfig.ssl.key, + cert: devServerConfig.ssl.cert, + ca: devServerConfig.ssl.ca, + }; + } + // If the server is being proxied, add the public host. + if (devServerConfig.proxied) { + config.devServer.public = devServerConfig.proxied.host; } // If the target will run with the dev server and it requires HMR... if (target.hot) { @@ -130,12 +143,9 @@ class WebpackBrowserDevelopmentConfiguration extends ConfigurationFile { config.devServer.publicPath = '/'; // Enable the dev server `hot` setting. config.devServer.hot = true; - // Build the host URL for the dev server as it will be needed for the hot entries. - const protocol = devServer.https ? 'https' : 'http'; - const host = `${protocol}://${devServerHost}:${config.devServer.port}`; // Push the required entries to enable HMR on the dev server. hotEntries.push(...[ - `webpack-dev-server/client?${host}`, + `webpack-dev-server/client?${devServerConfig.url}`, 'webpack/hot/only-dev-server', ]); } @@ -174,6 +184,56 @@ class WebpackBrowserDevelopmentConfiguration extends ConfigurationFile { params ); } + /** + * Check a target dev server settings in order to validate those that needs to be removed or + * completed with their default values. + * @param {Target} target The target information. + * @return {TargetDevServerSettings} + */ + _normalizeTargetDevServerSettings(target) { + // Get a new copy of the config to work with. + const config = extend(true, {}, target.devServer); + /** + * Set a flag to know if at least one SSL file was sent. + * This flag is also used when reading the `proxied` settings to determine the default + * behaviour of `proxied.https`. + */ + let hasASSLFile = false; + // Loop all the SSL files... + Object.keys(config.ssl).forEach((name) => { + const file = config.ssl[name]; + // If there's an actual path... + if (typeof file === 'string') { + // ...set the flag to `true`. + hasASSLFile = true; + // Generate the path to the file. + config.ssl[name] = this.pathUtils.join(file); + } + }); + // If no SSL file was sent, just remove the settings. + if (!hasASSLFile) { + delete config.ssl; + } + // If the server is being proxied... + if (config.proxied.enabled) { + // ...if no `host` was specified, use the one defined for the server. + if (config.proxied.host === null) { + config.proxied.host = config.host; + } + // If no `https` option was specified, set it to `true` if at least one SSL file was sent. + if (config.proxied.https === null) { + config.proxied.https = hasASSLFile; + } + } else { + // ...otherwise, just remove the setting. + delete config.proxied; + } + + const protocol = config.ssl ? 'https' : 'http'; + config.url = `${protocol}://${config.host}:${config.port}`; + + return config; + } /** * Creates a _'fake Webpack plugin'_ that detects when the bundle is being compiled in order to * log messages with the dev server information. diff --git a/src/typedef.js b/src/typedef.js index f016978..d49fccc 100644 --- a/src/typedef.js +++ b/src/typedef.js @@ -165,6 +165,43 @@ * The intended built type: `development` or `production`. */ +/** + * @typedef {Object} TargetDevServerSSLSettings + * @property {string} key + * The path to the SSL key (`.key`). + * @property {string} cert + * The path to the SSL certificate (`.crt`). + * @property {string} ca + * The path to the SSL public file (`.pem`). + */ + +/** + * @typedef {Object} TargetDevServerProxiedSettings + * @property {boolean} enabled + * Whether or not the dev server is being proxied. + * @property {string} host + * The host used to proxy the dev server. + * @property {boolean} https + * Whether or not the proxied host uses `https`. + */ + +/** + * @typedef {Object} TargetDevServerSettings + * @property {number} port + * The server port. + * @property {string} host + * The dev server hostname. + * @property {string} url + * The complete URL for the dev server. + * @property {boolean} reload + * Whether or not to reload the server when the code changes. + * @property {?TargetDevServerSSLSettings} ssl + * The paths to the files to enable SSL on the dev server. + * @property {?TargetDevServerProxiedSettings} [proxied] + * When the dev server is being proxied (using `nginx` for example), there are certain + * functionalities, like hot module replacement and live reload, that need to be aware of this. + */ + /** * @typedef {function} ProviderRegisterMethod * @param {Jimple} app diff --git a/tests/services/configurations/browserDevelopmentConfiguration.test.js b/tests/services/configurations/browserDevelopmentConfiguration.test.js index c86290d..c013275 100644 --- a/tests/services/configurations/browserDevelopmentConfiguration.test.js +++ b/tests/services/configurations/browserDevelopmentConfiguration.test.js @@ -254,7 +254,12 @@ describe('services/configurations:browserDevelopmentConfiguration', () => { const target = { name: 'targetName', runOnDevelopment: true, - devServer: {}, + devServer: { + port: 2509, + host: 'localhost', + ssl: {}, + proxied: {}, + }, folders: { build: 'build-folder', }, @@ -288,9 +293,10 @@ describe('services/configurations:browserDevelopmentConfiguration', () => { }; const expectedConfig = { devServer: { - port: 2509, + port: target.devServer.port, inline: false, open: true, + openPage: '/', hot: true, publicPath: '/', }, @@ -367,7 +373,7 @@ describe('services/configurations:browserDevelopmentConfiguration', () => { expect(appLogger.success).toHaveBeenCalledTimes(2); }); - it('should create a configuration for building and running the dev server (HTTPS)', () => { + it('should create a configuration for building and running the dev server (SSL)', () => { // Given const compiler = { plugin: jest.fn(), @@ -379,13 +385,21 @@ describe('services/configurations:browserDevelopmentConfiguration', () => { const events = { reduce: jest.fn((eventName, loaders) => loaders), }; - const pathUtils = 'pathUtils'; + const pathUtils = { + join: jest.fn((rest) => rest), + }; const webpackBaseConfiguration = 'webpackBaseConfiguration'; const target = { name: 'targetName', runOnDevelopment: true, devServer: { - https: true, + port: 2509, + host: 'localhost', + ssl: { + key: null, + cert: 'some/file.crt', + }, + proxied: {}, }, folders: { build: 'build-folder', @@ -397,7 +411,6 @@ describe('services/configurations:browserDevelopmentConfiguration', () => { template: 'index.html', }, sourceMap: {}, - hot: true, }; const definitions = 'definitions'; const babelPolyfillEntry = 'babel-polyfill'; @@ -419,17 +432,158 @@ describe('services/configurations:browserDevelopmentConfiguration', () => { output, }; const expectedConfig = { + devServer: { + port: target.devServer.port, + inline: false, + open: true, + openPage: '/', + https: target.devServer.ssl, + }, + entry: { + [target.name]: [ + babelPolyfillEntry, + targetEntry, + ], + }, + output: { + path: `./${target.folders.build}`, + filename: output.js, + publicPath: '/', + }, + plugins: expect.any(Array), + }; + let sut = null; + let result = null; + let devSeverPlugin = null; + let devSeverPluginCompile = null; + let devSeverPluginDone = null; + // When + sut = new WebpackBrowserDevelopmentConfiguration( + appLogger, + events, + pathUtils, + webpackBaseConfiguration + ); + result = sut.getConfig(params); + // Then + expect(result).toEqual(expectedConfig); + expect(ExtractTextPlugin).toHaveBeenCalledTimes(1); + expect(ExtractTextPlugin).toHaveBeenCalledWith(output.css); + expect(HtmlWebpackPlugin).toHaveBeenCalledTimes(1); + expect(HtmlWebpackPlugin).toHaveBeenCalledWith(Object.assign( + target.html, + { + template: `${target.paths.source}/${target.html.template}`, + inject: 'body', + } + )); + expect(ScriptExtHtmlWebpackPlugin).toHaveBeenCalledTimes(1); + expect(ScriptExtHtmlWebpackPlugin).toHaveBeenCalledWith({ + defaultAttribute: 'async', + }); + expect(webpackMock.NoEmitOnErrorsPluginMock).toHaveBeenCalledTimes(1); + expect(webpackMock.DefinePluginMock).toHaveBeenCalledTimes(1); + expect(webpackMock.DefinePluginMock).toHaveBeenCalledWith(definitions); + expect(OptimizeCssAssetsPlugin).toHaveBeenCalledTimes(1); + expect(pathUtils.join).toHaveBeenCalledTimes(1); + expect(pathUtils.join).toHaveBeenCalledWith(target.devServer.ssl.cert); + expect(events.reduce).toHaveBeenCalledTimes(1); + expect(events.reduce).toHaveBeenCalledWith( + 'webpack-browser-development-configuration', + expectedConfig, + params + ); + devSeverPlugin = result.plugins.slice().pop(); + devSeverPlugin.apply(compiler); + expect(compiler.plugin).toHaveBeenCalledTimes(['compile', 'done'].length); + expect(compiler.plugin).toHaveBeenCalledWith('compile', expect.any(Function)); + expect(compiler.plugin).toHaveBeenCalledWith('done', expect.any(Function)); + [ + [, devSeverPluginCompile], + [, devSeverPluginDone], + ] = compiler.plugin.mock.calls; + devSeverPluginCompile(); + expect(appLogger.success).toHaveBeenCalledTimes(1); + expect(appLogger.warning).toHaveBeenCalledTimes(1); + devSeverPluginDone(); + jest.runAllTimers(); + expect(appLogger.success).toHaveBeenCalledTimes(2); + }); + + it('should create a configuration for running the dev server with a custom host', () => { + // Given + const compiler = { + plugin: jest.fn(), + }; + const appLogger = { + success: jest.fn(), + warning: jest.fn(), + }; + const events = { + reduce: jest.fn((eventName, loaders) => loaders), + }; + const pathUtils = { + join: jest.fn((rest) => rest), + }; + const webpackBaseConfiguration = 'webpackBaseConfiguration'; + const target = { + name: 'targetName', + runOnDevelopment: true, devServer: { port: 2509, + host: 'my-host', + ssl: { + key: null, + cert: 'some/file.crt', + }, + proxied: {}, + }, + folders: { + build: 'build-folder', + }, + paths: { + source: 'source-path', + }, + html: { + template: 'index.html', + }, + sourceMap: {}, + hot: true, + }; + const definitions = 'definitions'; + const babelPolyfillEntry = 'babel-polyfill'; + const targetEntry = 'index.js'; + const entry = { + [target.name]: [ + babelPolyfillEntry, + targetEntry, + ], + }; + const output = { + js: 'statics/js/build.js', + css: 'statics/css/build.css', + }; + const params = { + target, + definitions, + entry, + output, + }; + const expectedConfig = { + devServer: { + port: target.devServer.port, inline: false, open: true, + openPage: '/', hot: true, publicPath: '/', + host: target.devServer.host, + https: target.devServer.ssl, }, entry: { [target.name]: [ babelPolyfillEntry, - 'webpack-dev-server/client?https://localhost:2509', + `webpack-dev-server/client?https://${target.devServer.host}:${target.devServer.port}`, 'webpack/hot/only-dev-server', targetEntry, ], @@ -476,6 +630,8 @@ describe('services/configurations:browserDevelopmentConfiguration', () => { expect(webpackMock.DefinePluginMock).toHaveBeenCalledTimes(1); expect(webpackMock.DefinePluginMock).toHaveBeenCalledWith(definitions); expect(OptimizeCssAssetsPlugin).toHaveBeenCalledTimes(1); + expect(pathUtils.join).toHaveBeenCalledTimes(1); + expect(pathUtils.join).toHaveBeenCalledWith(target.devServer.ssl.cert); expect(events.reduce).toHaveBeenCalledTimes(1); expect(events.reduce).toHaveBeenCalledWith( 'webpack-browser-development-configuration', @@ -499,7 +655,7 @@ describe('services/configurations:browserDevelopmentConfiguration', () => { expect(appLogger.success).toHaveBeenCalledTimes(2); }); - it('should create a configuration for running the dev server with a custom host', () => { + it('should create a configuration for running the dev server while proxied', () => { // Given const compiler = { plugin: jest.fn(), @@ -511,14 +667,22 @@ describe('services/configurations:browserDevelopmentConfiguration', () => { const events = { reduce: jest.fn((eventName, loaders) => loaders), }; - const pathUtils = 'pathUtils'; + const pathUtils = { + join: jest.fn((rest) => rest), + }; const webpackBaseConfiguration = 'webpackBaseConfiguration'; const target = { name: 'targetName', runOnDevelopment: true, devServer: { - https: true, - host: 'my-host', + port: 2509, + host: 'localhost', + ssl: {}, + proxied: { + enabled: true, + host: null, + https: null, + }, }, folders: { build: 'build-folder', @@ -553,9 +717,10 @@ describe('services/configurations:browserDevelopmentConfiguration', () => { }; const expectedConfig = { devServer: { - port: 2509, + port: target.devServer.port, inline: false, open: true, + openPage: '/', hot: true, publicPath: '/', public: target.devServer.host, @@ -563,7 +728,7 @@ describe('services/configurations:browserDevelopmentConfiguration', () => { entry: { [target.name]: [ babelPolyfillEntry, - `webpack-dev-server/client?https://${target.devServer.host}:2509`, + `webpack-dev-server/client?http://${target.devServer.host}:${target.devServer.port}`, 'webpack/hot/only-dev-server', targetEntry, ], @@ -633,7 +798,7 @@ describe('services/configurations:browserDevelopmentConfiguration', () => { expect(appLogger.success).toHaveBeenCalledTimes(2); }); - it('should create a configuration for building and running the dev server with HMR', () => { + it('should create a configuration for running the dev server proxied with a custom host', () => { // Given const compiler = { plugin: jest.fn(), @@ -645,12 +810,23 @@ describe('services/configurations:browserDevelopmentConfiguration', () => { const events = { reduce: jest.fn((eventName, loaders) => loaders), }; - const pathUtils = 'pathUtils'; + const pathUtils = { + join: jest.fn((rest) => rest), + }; const webpackBaseConfiguration = 'webpackBaseConfiguration'; const target = { name: 'targetName', runOnDevelopment: true, - devServer: {}, + devServer: { + port: 2509, + host: 'localhost', + ssl: {}, + proxied: { + enabled: true, + host: 'my-proxied-host', + https: false, + }, + }, folders: { build: 'build-folder', }, @@ -661,10 +837,16 @@ describe('services/configurations:browserDevelopmentConfiguration', () => { template: 'index.html', }, sourceMap: {}, + hot: true, }; const definitions = 'definitions'; + const babelPolyfillEntry = 'babel-polyfill'; + const targetEntry = 'index.js'; const entry = { - [target.name]: ['index.js'], + [target.name]: [ + babelPolyfillEntry, + targetEntry, + ], }; const output = { js: 'statics/js/build.js', @@ -678,11 +860,22 @@ describe('services/configurations:browserDevelopmentConfiguration', () => { }; const expectedConfig = { devServer: { - port: 2509, + port: target.devServer.port, inline: false, open: true, + openPage: '/', + hot: true, + publicPath: '/', + public: target.devServer.proxied.host, + }, + entry: { + [target.name]: [ + babelPolyfillEntry, + `webpack-dev-server/client?http://${target.devServer.host}:${target.devServer.port}`, + 'webpack/hot/only-dev-server', + targetEntry, + ], }, - entry, output: { path: `./${target.folders.build}`, filename: output.js, @@ -719,8 +912,8 @@ describe('services/configurations:browserDevelopmentConfiguration', () => { expect(ScriptExtHtmlWebpackPlugin).toHaveBeenCalledWith({ defaultAttribute: 'async', }); - expect(webpackMock.HotModuleReplacementPluginMock).toHaveBeenCalledTimes(0); - expect(webpackMock.NamedModulesPluginMock).toHaveBeenCalledTimes(0); + expect(webpackMock.HotModuleReplacementPluginMock).toHaveBeenCalledTimes(1); + expect(webpackMock.NamedModulesPluginMock).toHaveBeenCalledTimes(1); expect(webpackMock.NoEmitOnErrorsPluginMock).toHaveBeenCalledTimes(1); expect(webpackMock.DefinePluginMock).toHaveBeenCalledTimes(1); expect(webpackMock.DefinePluginMock).toHaveBeenCalledWith(definitions);