Home Reference Source

src/services/configurations/browserDevelopmentConfiguration.js

  1. /* eslint-disable complexity */
  2. const path = require('path');
  3. const ObjectUtils = require('wootils/shared/objectUtils');
  4. const HtmlWebpackPlugin = require('html-webpack-plugin');
  5. const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin');
  6. const MiniCssExtractPlugin = require('mini-css-extract-plugin');
  7. const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
  8. const CopyWebpackPlugin = require('copy-webpack-plugin');
  9. const ExtraWatchWebpackPlugin = require('extra-watch-webpack-plugin');
  10. const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
  11. const {
  12. NoEmitOnErrorsPlugin,
  13. HotModuleReplacementPlugin,
  14. NamedModulesPlugin,
  15. } = require('webpack');
  16. const { provider } = require('jimple');
  17. const ConfigurationFile = require('../../abstracts/configurationFile');
  18. const {
  19. ProjextWebpackOpenDevServer,
  20. ProjextWebpackRuntimeDefinitions,
  21. } = require('../../plugins');
  22. /**
  23. * Creates the specifics of a Webpack configuration for a browser target development build.
  24. * @extends {ConfigurationFile}
  25. */
  26. class WebpackBrowserDevelopmentConfiguration extends ConfigurationFile {
  27. /**
  28. * Class constructor.
  29. * @param {Logger} appLogger To send to the dev server plugin
  30. * in order to log its events.
  31. * @param {Events} events To reduce the configuration.
  32. * @param {PathUtils} pathUtils Required by `ConfigurationFile`
  33. * in order to build the path to the
  34. * overwrite file.
  35. * @param {TargetsHTML} targetsHTML The service in charge of generating
  36. * a default HTML file in case the
  37. * target doesn't have one.
  38. * @param {WebpackBaseConfiguration} webpackBaseConfiguration The configuration this one will
  39. * extend.
  40. * @param {WebpackPluginInfo} webpackPluginInfo To get the name of the plugin and
  41. * use it on the webpack hook that
  42. * logs the dev server URL when it
  43. * finishes bundling.
  44. */
  45. constructor(
  46. appLogger,
  47. events,
  48. pathUtils,
  49. targetsHTML,
  50. webpackBaseConfiguration,
  51. webpackPluginInfo
  52. ) {
  53. super(
  54. pathUtils,
  55. [
  56. 'config/webpack/browser.development.config.js',
  57. 'config/webpack/browser.config.js',
  58. ],
  59. true,
  60. webpackBaseConfiguration
  61. );
  62. /**
  63. * A local reference for the `appLogger` service.
  64. * @type {Logger}
  65. */
  66. this.appLogger = appLogger;
  67. /**
  68. * A local reference for the `events` service.
  69. * @type {Events}
  70. */
  71. this.events = events;
  72. /**
  73. * A local reference for the `targetsHTML` service.
  74. * @type {TargetsHTML}
  75. */
  76. this.targetsHTML = targetsHTML;
  77. /**
  78. * A local reference for the plugin information.
  79. * @type {WebpackPluginInfo}
  80. */
  81. this.webpackPluginInfo = webpackPluginInfo;
  82. }
  83. /**
  84. * Create the configuration with the `entry`, the `output` and the plugins specifics for a
  85. * browser target development build. It also checks if it should enable source map and the
  86. * dev server based on the target information.
  87. * This method uses the reducer events `webpack-browser-development-configuration` and
  88. * `webpack-browser-configuration`. It sends the configuration, the received `params` and
  89. * expects a configuration on return.
  90. * @param {WebpackConfigurationParams} params A dictionary generated by the top service building
  91. * the configuration and that includes things like the
  92. * target information, its entry settings, output
  93. * paths, etc.
  94. * @return {object}
  95. */
  96. createConfig(params) {
  97. const {
  98. definitions,
  99. copy,
  100. entry,
  101. target,
  102. output,
  103. additionalWatch,
  104. analyze,
  105. } = params;
  106. // Define the basic stuff: entry, output and mode.
  107. const config = {
  108. entry: ObjectUtils.copy(entry),
  109. output: {
  110. path: `./${target.folders.build}`,
  111. filename: output.js,
  112. chunkFilename: output.jsChunks,
  113. publicPath: '/',
  114. },
  115. mode: 'development',
  116. };
  117. // If the target has source maps enabled...
  118. if (target.sourceMap.development) {
  119. // ...configure the devtool
  120. config.devtool = 'source-map';
  121. }
  122. // Setup the plugins.
  123. config.plugins = [
  124. // To automatically inject the `script` tag on the target `html` file.
  125. new HtmlWebpackPlugin(Object.assign({}, target.html, {
  126. template: this.targetsHTML.getFilepath(target, false, 'development'),
  127. inject: 'body',
  128. })),
  129. // To add the `async` attribute to the `script` tag.
  130. new ScriptExtHtmlWebpackPlugin({
  131. defaultAttribute: 'async',
  132. }),
  133. // If the target uses hot replacement, add the plugin.
  134. ...(target.hot ? [new NamedModulesPlugin(), new HotModuleReplacementPlugin()] : []),
  135. // To avoid pushing assets with errors.
  136. new NoEmitOnErrorsPlugin(),
  137. // To add the _'browser env variables'_.
  138. new ProjextWebpackRuntimeDefinitions(
  139. Object.keys(entry).reduce(
  140. (current, key) => [...current, ...entry[key].filter((file) => path.isAbsolute(file))],
  141. []
  142. ),
  143. definitions
  144. ),
  145. // To optimize the SCSS and remove repeated declarations.
  146. new OptimizeCssAssetsPlugin(),
  147. // Copy the files the target specified on its settings.
  148. new CopyWebpackPlugin(copy),
  149. /**
  150. * If the target doesn't inject the styles on runtime, add the plugin to push them all on
  151. * a single file.
  152. */
  153. ...(
  154. target.css.inject ?
  155. [] :
  156. [new MiniCssExtractPlugin({
  157. filename: output.css,
  158. })]
  159. ),
  160. // If there are additionals files to watch, add the plugin for it.
  161. ...(
  162. additionalWatch.length ?
  163. [new ExtraWatchWebpackPlugin({ files: additionalWatch })] :
  164. []
  165. ),
  166. // If the the bundle should be analyzed, add the plugin for it.
  167. ...(
  168. analyze ?
  169. [new BundleAnalyzerPlugin()] :
  170. []
  171. ),
  172. ];
  173. // Define a list of extra entries that may be need depending on the target HMR configuration.
  174. const hotEntries = [];
  175. // If the target needs to run on development...
  176. if (!analyze && target.runOnDevelopment) {
  177. const devServerConfig = this._normalizeTargetDevServerSettings(target);
  178. // Add the dev server information to the configuration.
  179. config.devServer = {
  180. port: devServerConfig.port,
  181. inline: !!devServerConfig.reload,
  182. open: false,
  183. historyApiFallback: devServerConfig.historyApiFallback,
  184. };
  185. // If the configuration has a custom host, set it.
  186. if (devServerConfig.host !== 'localhost') {
  187. config.devServer.host = devServerConfig.host;
  188. }
  189. // If there are SSL files, set them on the server.
  190. if (devServerConfig.ssl) {
  191. config.devServer.https = {
  192. key: devServerConfig.ssl.key,
  193. cert: devServerConfig.ssl.cert,
  194. ca: devServerConfig.ssl.ca,
  195. };
  196. }
  197. // If the server is being proxied, add the public host.
  198. if (devServerConfig.proxied) {
  199. config.devServer.public = devServerConfig.proxied.host;
  200. }
  201. // If the target will run with the dev server and it requires HMR...
  202. if (target.hot) {
  203. // Disable the `inline` mode.
  204. config.devServer.inline = false;
  205. // Set the public path to `/`, as required by HMR.
  206. config.devServer.publicPath = '/';
  207. // Enable the dev server `hot` setting.
  208. config.devServer.hot = true;
  209. // Push the required entries to enable HMR on the dev server.
  210. hotEntries.push(...[
  211. `webpack-dev-server/client?${devServerConfig.url}`,
  212. 'webpack/hot/only-dev-server',
  213. ]);
  214. }
  215. // Push the plugin that logs the dev server statuses and opens the browser.
  216. config.plugins.push(new ProjextWebpackOpenDevServer(
  217. (devServerConfig.proxied ? devServerConfig.proxied.url : devServerConfig.url),
  218. {
  219. logger: this.appLogger,
  220. openBrowser: devServerConfig.open,
  221. }
  222. ));
  223. } else if (target.hot) {
  224. /**
  225. * If the target requires HMR but is not running with the dev server, it means that there's
  226. * an Express or Jimpex target that implements the `webpack-hot-middleware`, so we push it
  227. * required entry to the list.
  228. */
  229. hotEntries.push('webpack-hot-middleware/client?reload=true');
  230. } else if (target.watch.development) {
  231. /**
  232. * If the target is not running nor it requires HMR (which means is not being served either),
  233. * and the watch parameter is `true`, enable the watch mode.
  234. */
  235. config.watch = true;
  236. }
  237. // If there are entries for HMR...
  238. if (hotEntries.length) {
  239. // Get target entry name.
  240. const [entryName] = Object.keys(entry);
  241. // Get the list of entries for the target.
  242. const entries = config.entry[entryName];
  243. // and push all the _"hot entries"_ on top of the existing entries.
  244. entries.unshift(...hotEntries);
  245. }
  246.  
  247. // Reduce the configuration
  248. return this.events.reduce(
  249. [
  250. 'webpack-browser-development-configuration',
  251. 'webpack-browser-configuration',
  252. ],
  253. config,
  254. params
  255. );
  256. }
  257. /**
  258. * Check a target dev server settings in order to validate those that needs to be removed or
  259. * completed with their default values.
  260. * @param {Target} target The target information.
  261. * @return {TargetDevServerSettings}
  262. * @access protected
  263. * @ignore
  264. */
  265. _normalizeTargetDevServerSettings(target) {
  266. // Get a new copy of the config to work with.
  267. const config = ObjectUtils.copy(target.devServer);
  268. /**
  269. * Set a flag to know if at least one SSL file was sent.
  270. * This flag is also used when reading the `proxied` settings to determine the default
  271. * behaviour of `proxied.https`.
  272. */
  273. let hasASSLFile = false;
  274. // Loop all the SSL files...
  275. Object.keys(config.ssl).forEach((name) => {
  276. const file = config.ssl[name];
  277. // If there's an actual path...
  278. if (typeof file === 'string') {
  279. // ...set the flag to `true`.
  280. hasASSLFile = true;
  281. // Generate the path to the file.
  282. config.ssl[name] = this.pathUtils.join(file);
  283. }
  284. });
  285. // If no SSL file was sent, just remove the settings.
  286. if (!hasASSLFile) {
  287. delete config.ssl;
  288. }
  289. /**
  290. * Define whether to build a proxied URL for the plugin that opens the browser or not. The
  291. * reason for this is that when the server is proxied but the host is not defined, it will use
  292. * the dev server host, and in that case, it should include the port too, something that
  293. * wouldn't be necessary when the proxied host is specified.
  294. */
  295. let buildProxiedURL = true;
  296. // If the server is being proxied...
  297. if (config.proxied.enabled) {
  298. // ...if no `host` was specified, use the one defined for the server.
  299. if (config.proxied.host === null) {
  300. config.proxied.host = config.host;
  301. buildProxiedURL = false;
  302. }
  303. // If no `https` option was specified, set it to `true` if at least one SSL file was sent.
  304. if (config.proxied.https === null) {
  305. config.proxied.https = hasASSLFile;
  306. }
  307. // If a custom proxied host was specified, build the new URL.
  308. if (buildProxiedURL) {
  309. // Build the proxied URL.
  310. const proxiedProtocol = config.proxied.https ? 'https' : 'http';
  311. config.proxied.url = `${proxiedProtocol}://${config.proxied.host}`;
  312. }
  313. } else {
  314. // ...otherwise, just remove the setting.
  315. delete config.proxied;
  316. }
  317.  
  318. const protocol = config.ssl ? 'https' : 'http';
  319. config.url = `${protocol}://${config.host}:${config.port}`;
  320. /**
  321. * If the server is proxied, but without a custom host, copy the dev server URL into the
  322. * proxied settings.
  323. */
  324. if (config.proxied && !buildProxiedURL) {
  325. config.proxied.url = config.url;
  326. }
  327.  
  328. return config;
  329. }
  330. }
  331. /**
  332. * The service provider that once registered on the app container will set an instance of
  333. * `WebpackBrowserDevelopmentConfiguration` as the `webpackBrowserDevelopmentConfiguration` service.
  334. * @example
  335. * // Register it on the container
  336. * container.register(webpackBrowserDevelopmentConfiguration);
  337. * // Getting access to the service instance
  338. * const webpackBrowserDevConfig = container.get('webpackBrowserDevelopmentConfiguration');
  339. * @type {Provider}
  340. */
  341. const webpackBrowserDevelopmentConfiguration = provider((app) => {
  342. app.set(
  343. 'webpackBrowserDevelopmentConfiguration',
  344. () => new WebpackBrowserDevelopmentConfiguration(
  345. app.get('appLogger'),
  346. app.get('events'),
  347. app.get('pathUtils'),
  348. app.get('targetsHTML'),
  349. app.get('webpackBaseConfiguration'),
  350. app.get('webpackPluginInfo')
  351. )
  352. );
  353. });
  354.  
  355. module.exports = {
  356. WebpackBrowserDevelopmentConfiguration,
  357. webpackBrowserDevelopmentConfiguration,
  358. };