src/services/configurations/browserDevelopmentConfiguration.js
/* eslint-disable complexity */
const path = require('path');
const ObjectUtils = require('wootils/shared/objectUtils');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ExtraWatchWebpackPlugin = require('extra-watch-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const {
NoEmitOnErrorsPlugin,
HotModuleReplacementPlugin,
NamedModulesPlugin,
} = require('webpack');
const { provider } = require('jimple');
const ConfigurationFile = require('../../abstracts/configurationFile');
const {
ProjextWebpackOpenDevServer,
ProjextWebpackRuntimeDefinitions,
} = require('../../plugins');
/**
* Creates the specifics of a Webpack configuration for a browser target development build.
* @extends {ConfigurationFile}
*/
class WebpackBrowserDevelopmentConfiguration extends ConfigurationFile {
/**
* Class constructor.
* @param {Logger} appLogger To send to the dev server plugin
* in order to log its events.
* @param {Events} events To reduce the configuration.
* @param {PathUtils} pathUtils Required by `ConfigurationFile`
* in order to build the path to the
* overwrite file.
* @param {TargetsHTML} targetsHTML The service in charge of generating
* a default HTML file in case the
* target doesn't have one.
* @param {WebpackBaseConfiguration} webpackBaseConfiguration The configuration this one will
* extend.
* @param {WebpackPluginInfo} webpackPluginInfo To get the name of the plugin and
* use it on the webpack hook that
* logs the dev server URL when it
* finishes bundling.
*/
constructor(
appLogger,
events,
pathUtils,
targetsHTML,
webpackBaseConfiguration,
webpackPluginInfo
) {
super(
pathUtils,
[
'config/webpack/browser.development.config.js',
'config/webpack/browser.config.js',
],
true,
webpackBaseConfiguration
);
/**
* A local reference for the `appLogger` service.
* @type {Logger}
*/
this.appLogger = appLogger;
/**
* A local reference for the `events` service.
* @type {Events}
*/
this.events = events;
/**
* A local reference for the `targetsHTML` service.
* @type {TargetsHTML}
*/
this.targetsHTML = targetsHTML;
/**
* A local reference for the plugin information.
* @type {WebpackPluginInfo}
*/
this.webpackPluginInfo = webpackPluginInfo;
}
/**
* Create the configuration with the `entry`, the `output` and the plugins specifics for a
* browser target development build. It also checks if it should enable source map and the
* dev server based on the target information.
* This method uses the reducer events `webpack-browser-development-configuration` and
* `webpack-browser-configuration`. It sends the configuration, the received `params` and
* expects a configuration on return.
* @param {WebpackConfigurationParams} params A dictionary generated by the top service building
* the configuration and that includes things like the
* target information, its entry settings, output
* paths, etc.
* @return {object}
*/
createConfig(params) {
const {
definitions,
copy,
entry,
target,
output,
additionalWatch,
analyze,
} = params;
// Define the basic stuff: entry, output and mode.
const config = {
entry: ObjectUtils.copy(entry),
output: {
path: `./${target.folders.build}`,
filename: output.js,
chunkFilename: output.jsChunks,
publicPath: '/',
},
mode: 'development',
};
// If the target has source maps enabled...
if (target.sourceMap.development) {
// ...configure the devtool
config.devtool = 'source-map';
}
// Setup the plugins.
config.plugins = [
// To automatically inject the `script` tag on the target `html` file.
new HtmlWebpackPlugin(Object.assign({}, target.html, {
template: this.targetsHTML.getFilepath(target, false, 'development'),
inject: 'body',
})),
// To add the `async` attribute to the `script` tag.
new ScriptExtHtmlWebpackPlugin({
defaultAttribute: 'async',
}),
// If the target uses hot replacement, add the plugin.
...(target.hot ? [new NamedModulesPlugin(), new HotModuleReplacementPlugin()] : []),
// To avoid pushing assets with errors.
new NoEmitOnErrorsPlugin(),
// To add the _'browser env variables'_.
new ProjextWebpackRuntimeDefinitions(
Object.keys(entry).reduce(
(current, key) => [...current, ...entry[key].filter((file) => path.isAbsolute(file))],
[]
),
definitions
),
// To optimize the SCSS and remove repeated declarations.
new OptimizeCssAssetsPlugin(),
// Copy the files the target specified on its settings.
new CopyWebpackPlugin(copy),
/**
* If the target doesn't inject the styles on runtime, add the plugin to push them all on
* a single file.
*/
...(
target.css.inject ?
[] :
[new MiniCssExtractPlugin({
filename: output.css,
})]
),
// If there are additionals files to watch, add the plugin for it.
...(
additionalWatch.length ?
[new ExtraWatchWebpackPlugin({ files: additionalWatch })] :
[]
),
// If the the bundle should be analyzed, add the plugin for it.
...(
analyze ?
[new BundleAnalyzerPlugin()] :
[]
),
];
// Define a list of extra entries that may be need depending on the target HMR configuration.
const hotEntries = [];
// If the target needs to run on development...
if (!analyze && target.runOnDevelopment) {
const devServerConfig = this._normalizeTargetDevServerSettings(target);
// Add the dev server information to the configuration.
config.devServer = {
port: devServerConfig.port,
inline: !!devServerConfig.reload,
open: false,
historyApiFallback: devServerConfig.historyApiFallback,
};
// 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) {
// Disable the `inline` mode.
config.devServer.inline = false;
// Set the public path to `/`, as required by HMR.
config.devServer.publicPath = '/';
// Enable the dev server `hot` setting.
config.devServer.hot = true;
// Push the required entries to enable HMR on the dev server.
hotEntries.push(...[
`webpack-dev-server/client?${devServerConfig.url}`,
'webpack/hot/only-dev-server',
]);
}
// Push the plugin that logs the dev server statuses and opens the browser.
config.plugins.push(new ProjextWebpackOpenDevServer(
(devServerConfig.proxied ? devServerConfig.proxied.url : devServerConfig.url),
{
logger: this.appLogger,
openBrowser: devServerConfig.open,
}
));
} else if (target.hot) {
/**
* If the target requires HMR but is not running with the dev server, it means that there's
* an Express or Jimpex target that implements the `webpack-hot-middleware`, so we push it
* required entry to the list.
*/
hotEntries.push('webpack-hot-middleware/client?reload=true');
} else if (target.watch.development) {
/**
* If the target is not running nor it requires HMR (which means is not being served either),
* and the watch parameter is `true`, enable the watch mode.
*/
config.watch = true;
}
// If there are entries for HMR...
if (hotEntries.length) {
// Get target entry name.
const [entryName] = Object.keys(entry);
// Get the list of entries for the target.
const entries = config.entry[entryName];
// and push all the _"hot entries"_ on top of the existing entries.
entries.unshift(...hotEntries);
}
// Reduce the configuration
return this.events.reduce(
[
'webpack-browser-development-configuration',
'webpack-browser-configuration',
],
config,
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}
* @access protected
* @ignore
*/
_normalizeTargetDevServerSettings(target) {
// Get a new copy of the config to work with.
const config = ObjectUtils.copy(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;
}
/**
* Define whether to build a proxied URL for the plugin that opens the browser or not. The
* reason for this is that when the server is proxied but the host is not defined, it will use
* the dev server host, and in that case, it should include the port too, something that
* wouldn't be necessary when the proxied host is specified.
*/
let buildProxiedURL = true;
// 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;
buildProxiedURL = false;
}
// 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;
}
// If a custom proxied host was specified, build the new URL.
if (buildProxiedURL) {
// Build the proxied URL.
const proxiedProtocol = config.proxied.https ? 'https' : 'http';
config.proxied.url = `${proxiedProtocol}://${config.proxied.host}`;
}
} else {
// ...otherwise, just remove the setting.
delete config.proxied;
}
const protocol = config.ssl ? 'https' : 'http';
config.url = `${protocol}://${config.host}:${config.port}`;
/**
* If the server is proxied, but without a custom host, copy the dev server URL into the
* proxied settings.
*/
if (config.proxied && !buildProxiedURL) {
config.proxied.url = config.url;
}
return config;
}
}
/**
* The service provider that once registered on the app container will set an instance of
* `WebpackBrowserDevelopmentConfiguration` as the `webpackBrowserDevelopmentConfiguration` service.
* @example
* // Register it on the container
* container.register(webpackBrowserDevelopmentConfiguration);
* // Getting access to the service instance
* const webpackBrowserDevConfig = container.get('webpackBrowserDevelopmentConfiguration');
* @type {Provider}
*/
const webpackBrowserDevelopmentConfiguration = provider((app) => {
app.set(
'webpackBrowserDevelopmentConfiguration',
() => new WebpackBrowserDevelopmentConfiguration(
app.get('appLogger'),
app.get('events'),
app.get('pathUtils'),
app.get('targetsHTML'),
app.get('webpackBaseConfiguration'),
app.get('webpackPluginInfo')
)
);
});
module.exports = {
WebpackBrowserDevelopmentConfiguration,
webpackBrowserDevelopmentConfiguration,
};