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,
};