Home Reference Source

src/services/configurations/rulesConfiguration.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { provider } = require('jimple');
const ConfigurationFile = require('../../abstracts/configurationFile');
/**
 * Define the Webpack configuration rules for basic types of assets: Javascript, stylesheets,
 * images and fonts.
 * @extends {ConfigurationFile}
 */
class WebpackRulesConfiguration extends ConfigurationFile {
  /**
   * Class constructor.
   * @param {BabelConfiguration} babelConfiguration   Used to configure the `babel-loader`.
   * @param {Events}             events               To reduce each set of rules and the entire
   *                                                  configuration.
   * @param {Object}             packageInfo          To read the dependencies list.
   * @param {PathUtils}          pathUtils            Required by `ConfigurationFile` in order to
   *                                                  build the path to the overwrite file.
   */
  constructor(babelConfiguration, events, packageInfo, pathUtils) {
    super(pathUtils, 'webpack/rules.config.js');
    /**
     * A local reference for the `babelConfiguration` service.
     * @type {BabelConfiguration}
     */
    this.babelConfiguration = babelConfiguration;
    /**
     * A local reference for the `events` service.
     * @type {Events}
     */
    this.events = events;
    /**
     * The name of the loader for image optimization. This is on a property because the service
     * will eveluate if the loader is installed before adding specific rules for it.
     * @type {String}
     * @access protected
     * @ignore
     */
    this._imageLoaderName = 'image-webpack-loader';
    /**
     * Whether or not the implementation has the load for image optimization installed.
     * @type {Boolean}
     * @access protected
     * @ignore
     */
    this._hasImageLoader = this._detectImageLoader(packageInfo);
  }
  /**
   * Creates the rules configuration for the required target.
   * This method uses the reducer events `webpack-rules-configuration-for-node` or
   * `webpack-rules-configuration-for-browser`, depending on the target type, and then
   * `webpack-rules-configuration`. The event receives the configuration object, the `params` and
   * it expects an updated configuration object 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}
   * @property {Array} rules The list of rules
   */
  createConfig(params) {
    const rules = [
      ...this._getJSRules(params),
      ...this._getSCSSRules(params),
      ...this._getCSSRules(params),
      ...this._getHTMLRules(params),
      ...this._getFontsRules(params),
      ...this._getImagesRules(params),
      ...this._getFaviconsRules(params),
    ];

    const eventName = params.target.is.node ?
      'webpack-rules-configuration-for-node' :
      'webpack-rules-configuration-for-browser';
    return this.events.reduce(
      [eventName, 'webpack-rules-configuration'],
      { rules },
      params
    );
  }
  /**
   * Checks whether or not a `package.json` includes the image loader as a dependency or dev
   * dependency.
   * @param {Object} packageInfo The contents of a `package.json`.
   * @return {Boolean}
   * @access protected
   * @ignore
   */
  _detectImageLoader(packageInfo) {
    const names = [];
    ['dependencies', 'devDependencies'].forEach((dependencyType) => {
      if (packageInfo[dependencyType]) {
        names.push(...Object.keys(packageInfo[dependencyType]));
      }
    });

    return names.some((name) => name === this._imageLoaderName);
  }
  /**
   * Defines the list of rules for Javascript files.
   * This method uses the reducer event `webpack-js-rules-configuration-for-browser` or
   * `webpack-js-rules-configuration-for-node`, depending on the target type, and then
   * `webpack-js-rules-configuration`. The event receives the rules, the `params` and expects a
   * rules list 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 {Array}
   * @access protected
   * @ignore
   */
  _getJSRules(params) {
    const { target, targetRules } = params;
    const jsRule = targetRules.js.getRule();
    const rules = [{
      test: jsRule.extension,
      include: [
        ...jsRule.files.include,
      ],
      exclude: [
        ...jsRule.files.exclude,
      ],
      use: [{
        loader: 'babel-loader',
        // Apply the target's own Babel configuration.
        options: this.babelConfiguration.getConfigForTarget(target),
      }],
    }];
    // Reduce the rules.
    const eventName = target.is.node ?
      'webpack-js-rules-configuration-for-node' :
      'webpack-js-rules-configuration-for-browser';
    return this.events.reduce(
      [eventName, 'webpack-js-rules-configuration'],
      rules,
      params
    );
  }
  /**
   * Define the list of rules for SCSS stylesheets.
   * This method uses the reducer event `webpack-scss-rules-configuration-for-browser` or
   * `webpack-scss-rules-configuration-for-node`, depending on the target type, and then
   * `webpack-scss-rules-configuration`. The event receives the rules, the `params` and expects a
   * rules list 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 {Array}
   * @access protected
   * @ignore
   */
  _getSCSSRules(params) {
    const { target, targetRules } = params;
    const scssRule = targetRules.scss.getRule();
    // Set the base configuration for the CSS loader.
    const cssLoaderConfig = {
      // `2` because there are two other loaders after it: `resolve-url-loader` and `sass-loader`.
      importLoaders: 2,
    };
    // If the target uses CSS modules...
    if (target.css.modules) {
      // ...enable them on the CSS loader configuration and add the name format.
      cssLoaderConfig.modules = {
        localIdentName: '[name]__[local]___[hash:base64:5]',
      };
    }

    let eventName = 'webpack-scss-rules-configuration-for-node';
    const use = [
      {
        loader: 'css-loader',
        options: cssLoaderConfig,
      },
      'resolve-url-loader',
      {
        loader: 'sass-loader',
        options: {
          /**
           * This is necessary for the `resolve-url-loader` to be able to find and fix the
           * relative paths for linked assets.
           */
          sourceMap: true,
          sassOptions: {
            outputStyle: 'expanded',
            includePaths: ['node_modules'],
          },
        },
      },
    ];
    if (target.is.browser) {
      eventName = 'webpack-scss-rules-configuration-for-browser';
      // If the target needs to inject the styles on the `<head>`...
      if (target.css.inject) {
        // ...add the style loader.
        use.unshift('style-loader');
      } else {
        // ...otherwise, push first the loader for th eplugin that creates a single stylesheet.
        use.unshift(MiniCssExtractPlugin.loader);
      }
    }

    const rules = [{
      test: scssRule.extension,
      include: [
        ...scssRule.files.include,
      ],
      exclude: [
        ...scssRule.files.exclude,
      ],
      use,
    }];
    // Reduce the rules.
    return this.events.reduce(
      [eventName, 'webpack-scss-rules-configuration'],
      rules,
      params
    );
  }
  /**
   * Define the list of rules for CSS stylesheets.
   * This method uses the reducer event `webpack-css-rules-configuration-for-browser` or
   * `webpack-css-rules-configuration-for-node`, depending on the target type, and then
   * `webpack-css-rules-configuration`. The event receives the rules, the `params` and expects a
   * rules list 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 {Array}
   * @access protected
   * @ignore
   */
  _getCSSRules(params) {
    const { target, targetRules } = params;
    const cssRule = targetRules.css.getRule();
    let eventName = 'webpack-css-rules-configuration-for-node';
    const use = [
      'css-loader',
    ];

    if (target.is.browser) {
      eventName = 'webpack-css-rules-configuration-for-browser';
      // If the target needs to inject the styles on the `<head>`...
      if (target.css.inject) {
        // ...add the style loader.
        use.unshift('style-loader');
      } else {
        // ...otherwise, push first the loader for th eplugin that creates a single stylesheet.
        use.unshift(MiniCssExtractPlugin.loader);
      }
    }

    const rules = [{
      test: cssRule.extension,
      include: [
        ...cssRule.files.include,
      ],
      exclude: [
        ...cssRule.files.exclude,
      ],
      use,
    }];
    // Reduce the rules.
    return this.events.reduce(
      [eventName, 'webpack-css-rules-configuration'],
      rules,
      params
    );
  }
  /**
   * Define the list of rules for HTML files.
   * This method uses the reducer event `webpack-html-rules-configuration-for-browser` or
   * `webpack-html-rules-configuration-for-node`, depending on the target type, and then
   * `webpack-html-rules-configuration`. The event receives the rules, the `params` and expects a
   * rules list 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 {Array}
   * @access protected
   * @ignore
   */
  _getHTMLRules(params) {
    const rules = [{
      test: /\.html?$/,
      // Avoid template files.
      exclude: /\.tpl\.html/,
      use: [
        'raw-loader',
      ],
    }];
    // Reduce the rules.
    const eventName = params.target.is.node ?
      'webpack-html-rules-configuration-for-node' :
      'webpack-html-rules-configuration-for-browser';
    return this.events.reduce(
      [eventName, 'webpack-html-rules-configuration'],
      rules,
      params
    );
  }
  /**
   * Define the list of rules for font files.
   * This method uses the reducer event `webpack-fonts-rules-configuration-for-browser` or
   * `webpack-fonts-rules-configuration-for-node`, depending on the target type, and then
   * `webpack-fonts-rules-configuration`. The event receives the rules, the `params` and expects a
   * rules list 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 {Array}
   * @access protected
   * @ignore
   */
  _getFontsRules(params) {
    const { target, targetRules, output: { fonts: name } } = params;
    const commonFontsRule = targetRules.fonts.common.getRule();
    const svgFontsRule = targetRules.fonts.svg.getRule();
    const use = [{
      loader: 'file-loader',
      options: {
        name,
      },
    }];
    const rules = [
      {
        test: commonFontsRule.extension,
        include: [
          ...commonFontsRule.files.include,
        ],
        exclude: [
          ...commonFontsRule.files.exclude,
        ],
        use,
      },
      {
        test: svgFontsRule.extension,
        include: [
          ...svgFontsRule.files.include,
        ],
        exclude: [
          ...svgFontsRule.files.exclude,
        ],
        use,
      },
    ];
    // Reduce the rules.
    const eventName = target.is.node ?
      'webpack-fonts-rules-configuration-for-node' :
      'webpack-fonts-rules-configuration-for-browser';
    return this.events.reduce(
      [eventName, 'webpack-fonts-rules-configuration'],
      rules,
      params
    );
  }
  /**
   * Define the list of rules for images files.
   * This method uses the reducer event `webpack-images-rules-configuration-for-browser` or
   * `webpack-images-rules-configuration-for-node`, depending on the target type, and then
   * `webpack-images-rules-configuration`. The event receives the rules, the `params` and expects a
   * rules list 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 {Array}
   * @access protected
   * @ignore
   */
  _getImagesRules(params) {
    const { target, targetRules, output: { images: name } } = params;
    const imagesRule = targetRules.images.getRule();
    const use = [{
      loader: 'file-loader',
      options: {
        name,
      },
    }];
    if (this._hasImageLoader) {
      use.push({
        loader: this._imageLoaderName,
        options: {
          mozjpeg: {
            progressive: true,
          },
          gifsicle: {
            interlaced: false,
          },
          optipng: {
            optimizationLevel: 7,
          },
          pngquant: {
            quality: '75-90',
            speed: 3,
          },
        },
      });
    }
    const rules = [{
      test: imagesRule.extension,
      include: [
        ...imagesRule.files.include,
      ],
      exclude: [
        ...imagesRule.files.exclude,
      ],
      use,
    }];
    // Reduce the rules.
    const eventName = target.is.node ?
      'webpack-images-rules-configuration-for-node' :
      'webpack-images-rules-configuration-for-browser';
    return this.events.reduce(
      [eventName, 'webpack-images-rules-configuration'],
      rules,
      params
    );
  }
  /**
   * Define the list of rules for the favicons file.
   * The reason this is not with the images rules is because favicons need to be on the root
   * directory for the browser to automatically detect them, and they only include optimization
   * options for `png`.
   * This method uses the reducer event `webpack-favicons-rules-configuration-for-browser` or
   * `webpack-favicons-rules-configuration-for-node`, depending on the target type, and then
   * `webpack-favicons-rules-configuration`. The event receives the rules, the `params` and expects
   * a rules list 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 {Array}
   * @access protected
   * @ignore
   */
  _getFaviconsRules(params) {
    const { target, targetRules } = params;
    const faviconRule = targetRules.favicon.getRule();
    const use = [{
      loader: 'file-loader',
      options: {
        name: '[name].[ext]',
      },
    }];
    if (this._hasImageLoader) {
      use.push({
        loader: this._imageLoaderName,
        options: {
          optipng: {
            optimizationLevel: 7,
          },
          pngquant: {
            quality: '75-90',
            speed: 3,
          },
        },
      });
    }
    const rules = [{
      test: faviconRule.extension,
      include: [
        ...faviconRule.files.include,
      ],
      exclude: [
        ...faviconRule.files.exclude,
      ],
      use,
    }];
    // Reduce the rules.
    const eventName = target.is.node ?
      'webpack-favicons-rules-configuration-for-node' :
      'webpack-favicons-rules-configuration-for-browser';
    return this.events.reduce(
      [eventName, 'webpack-favicons-rules-configuration'],
      rules,
      params
    );
  }
}
/**
 * The service provider that once registered on the app container will set an instance of
 * `WebpackRulesConfiguration` as the `webpackRulesConfiguration` service.
 * @example
 * // Register it on the container
 * container.register(webpackRulesConfiguration);
 * // Getting access to the service instance
 * const webpackRulesConfiguration = container.get('webpackRulesConfiguration');
 * @type {Provider}
 */
const webpackRulesConfiguration = provider((app) => {
  app.set('webpackRulesConfiguration', () => new WebpackRulesConfiguration(
    app.get('babelConfiguration'),
    app.get('events'),
    app.get('packageInfo'),
    app.get('pathUtils')
  ));
});

module.exports = {
  WebpackRulesConfiguration,
  webpackRulesConfiguration,
};