src/services/configurations/babelConfiguration.js
const { provider } = require('jimple');
/**
* This service is in charge of creating Babel configurations for targets.
*/
class BabelConfiguration {
/**
* Class constructor.
* @param {Events} events To reduce the configurations.
*/
constructor(events) {
/**
* A local reference for the `events` service.
* @type {Events}
*/
this.events = events;
/**
* A dictionary with familiar names for Babel plugins.
* @type {Object}
* @access protected
* @ignore
*/
this._plugins = {
decorators: {
name: '@babel/plugin-proposal-decorators',
options: {
legacy: true,
},
},
classProperties: {
name: '@babel/plugin-proposal-class-properties',
options: {
loose: true,
},
},
dynamicImports: {
name: '@babel/plugin-syntax-dynamic-import',
options: {},
},
objectRestSpread: {
name: '@babel/plugin-proposal-object-rest-spread',
options: {},
},
};
/**
* A dictionary with familiar names for Babel presets for type check.
* @type {Object}
* @access protected
* @ignore
*/
this._typesPresets = {
flow: '@babel/preset-flow',
typeScript: '@babel/preset-typescript',
};
}
/**
* Get a Babel configuration for a target.
* This method uses the event reducer `babel-configuration`, which sends a Babel configuration
* and a target information, and expects a Babel configuration on return.
* @param {Target} target The target information.
* @return {Object}
*/
getConfigForTarget(target) {
// Get the target settings we need.
const {
babel: {
features,
overwrites,
},
flow,
typeScript,
framework,
} = target;
// Define the configuration we are going to _'update'_.
const config = Object.assign({}, overwrites || {});
// Define the list of presets.
const presets = config.presets || [];
// Define the list of plugins.
const plugins = config.plugins || [];
// Define the name of `env` preset; to avoid having the string on multiple places.
const envPresetName = '@babel/preset-env';
// Check whether or not the presets include the `env` preset.
const hasEnv = presets
.find((preset) => (Array.isArray(preset) && preset[0] === envPresetName));
// If it doesn't have the `env` preset...
if (!hasEnv) {
// ... create an `env` preset for the target and add it to the top of the list.
presets.unshift([envPresetName, this._createEnvPresetForTarget(target)]);
}
// Check if the configuration should include any _'known plugin'_.
Object.keys(features).forEach((feature) => {
if (features[feature] && this._plugins[feature]) {
const featurePlugin = this._plugins[feature];
if (!this._includesConfigurationItem(plugins, featurePlugin.name)) {
if (Object.keys(featurePlugin.options).length) {
plugins.push([featurePlugin.name, featurePlugin.options]);
} else {
plugins.push(featurePlugin.name);
}
}
}
});
// Check if the target uses Flow or TypeScript.
if (flow) {
const flowConfig = this._getFlowConfiguration({ presets, plugins });
presets.push(...flowConfig.presets);
plugins.push(...flowConfig.plugins);
} else if (typeScript) {
const tsConfig = this._getTypeScriptConfiguration({ presets, plugins }, framework);
presets.push(...tsConfig.presets);
plugins.push(...tsConfig.plugins);
}
// Set both presets and plugins back on the config.
config.presets = presets;
config.plugins = plugins;
// Return a reduced configuration
return this.events.reduce('babel-configuration', config, target);
}
/**
* Creates a configuration for a Babel "env preset" using the settings from a target.
* @param {Target} target The target information.
* @return {Object}
* @access protected
* @ignore
*/
_createEnvPresetForTarget(target) {
// Get the target settings we need.
const {
babel: {
nodeVersion,
browserVersions,
mobileSupport,
polyfill,
env,
},
} = target;
/**
* If the target needs polyfills, use as base the settings for `core-js`, otherwise, just
* use an empty object.
*/
const presetBaseSettings = polyfill ? { corejs: 3, useBuiltIns: 'usage' } : {};
/**
* Merge an object with the required properties, the base generated after evaluating the need
* for polyfills, and whatever was specified on the target settings.
*/
const envPreset = Object.assign({ targets: {} }, presetBaseSettings, env);
// If the target is for browsers...
if (target.is.browser) {
/**
* Check if the target had settings for browsers, because if there are no settings, the
* method will create new ones, if there was an array, the method will only add settings
* for browsers that are not present; and if the value is `falsy`, it will delete the key.
*/
const { targets: { browsers: currentBrowsers } } = envPreset;
const currentBrowsersExists = Array.isArray(currentBrowsers);
if (currentBrowsersExists || typeof currentBrowsers === 'undefined') {
// Define the list of basic desktop browsers.
const browsers = ['chrome', 'safari', 'edge', 'firefox'];
// If the target needs transpilation for mobile, add the supported mobile browsers.
if (mobileSupport) {
browsers.push(...['ios', 'android']);
}
/**
* Map the settings into dictionaries with the name of the browser the setting is for and
* the value of the setting.
*/
let browsersSettings = browsers.map((browser) => ({
name: browser,
setting: `last ${browserVersions} ${browser} versions`,
}));
// Define the variable for the new value of the setting.
const newValue = [];
/**
* If there was a list of browser settings on the target, push it to the list that will be
* used as the new value and remove the browsers that are already present.
*/
if (currentBrowsersExists) {
newValue.push(...currentBrowsers);
browsersSettings = browsersSettings
.filter((settings) => !currentBrowsers.some((line) => line.includes(settings.name)));
}
// Push the settings for the list of browsers generated by the method.
newValue.push(...browsersSettings.map(({ setting }) => setting));
// Overwrite the value of the setting.
envPreset.targets.browsers = newValue;
} else {
// `browsers` was `falsy`, so it needs to be removed.
delete envPreset.targets.browsers;
}
} else if (typeof envPreset.targets.node === 'undefined') {
// Add the Node version if it's not already defined.
envPreset.targets.node = nodeVersion;
}
return envPreset;
}
/**
* Checks if a plugin/preset exists on a Babel configuration property list. The reason of the
* method is that, sometimes, the plugins or presets can be defined as array (first the name and
* then the options), so it also needs to check for those cases.
* @param {Array} configurationList The list of presets or plugins where the function will look
* for the item.
* @param {string} item The name of the item the function needs to check for.
* @return {boolean}
* @access protected
* @ignore
*/
_includesConfigurationItem(configurationList, item) {
return configurationList.length ?
configurationList.find((element) => (
Array.isArray(element) && element.length ?
element[0] === item :
element === item
)) :
false;
}
/**
* This method will generate a list of presets and plugins needed to support Flow on a
* given Babel configuration. To avoid modifying the reference of the current configuration or
* generating a new one for overwriting, the method will generate two new lists that can be
* pushed directly to the existing configuration.
* @example
* const flowConfig = this._getFlowConfiguration(currentConfig);
* currentConfig.presets.push(...flowConfig.presets);
* currentConfig.plugins.push(...flowConfig.plugins);
* @param {Object} currentConfiguration The configuration to validate.
* @param {Array} currentConfiguration.presets The current list of presets.
* @param {Array} currentConfiguration.plugins The current list of plugins.
* @return {Object} And object with missing plugins and presets to achieve support for Flow.
* @property {Array} presets The list of missing presets needed to support Flow.
* @property {Array} plugins The list of missing presets needed to support Flow.
* @access protected
* @ignore
*/
_getFlowConfiguration(currentConfiguration) {
const newConfig = {
presets: [],
plugins: [],
};
if (!this._includesConfigurationItem(
currentConfiguration.presets,
this._typesPresets.flow
)) {
newConfig.presets.push([this._typesPresets.flow]);
}
if (!this._includesConfigurationItem(
currentConfiguration.plugins,
this._plugins.classProperties.name
)) {
const { classProperties } = this._plugins;
newConfig.plugins.push([classProperties.name, classProperties.options]);
}
return newConfig;
}
/**
* This method will generate a list of presets and plugins needed to support TypeScript on a
* given Babel configuration. To avoid modifying the reference of the current configuration or
* generating a new one for overwriting, the method will generate two new lists that can be
* pushed directly to the existing configuration.
* @example
* const tsConfig = this._getTypeScriptConfiguration(currentConfig, framework);
* currentConfig.presets.push(...tsConfig.presets);
* currentConfig.plugins.push(...tsConfig.plugins);
* @param {Object} currentConfiguration The configuration to validate.
* @param {Array} currentConfiguration.presets The current list of presets.
* @param {Array} currentConfiguration.plugins The current list of plugins.
* @param {String} framework To check for React and enable TSX support.
* @return {Object} And object with missing plugins and presets to achieve support for TypeScript.
* @property {Array} presets The list of missing presets needed to support TypeScript.
* @property {Array} plugins The list of missing presets needed to support TypeScript.
* @access protected
* @ignore
*/
_getTypeScriptConfiguration(currentConfiguration, framework) {
const newConfig = {
presets: [],
plugins: [],
};
if (!this._includesConfigurationItem(
currentConfiguration.presets,
this._typesPresets.typeScript
)) {
const tsOptions = {};
if (framework === 'react') {
tsOptions.isTSX = true;
tsOptions.allExtensions = true;
}
newConfig.presets.push([this._typesPresets.typeScript, tsOptions]);
}
const toAdd = [];
if (!this._includesConfigurationItem(
currentConfiguration.plugins,
this._plugins.classProperties.name
)) {
toAdd.push('classProperties');
}
if (!this._includesConfigurationItem(
currentConfiguration.plugins,
this._plugins.objectRestSpread.name
)) {
toAdd.push('objectRestSpread');
}
toAdd.forEach((feature) => {
const featurePlugin = this._plugins[feature];
if (Object.keys(featurePlugin.options).length) {
newConfig.plugins.push([featurePlugin.name, featurePlugin.options]);
} else {
newConfig.plugins.push(featurePlugin.name);
}
});
return newConfig;
}
}
/**
* The service provider that once registered on the app container will set an instance of
* `BabelConfiguration` as the `babelConfiguration` service.
* @example
* // Register it on the container
* container.register(babelConfiguration);
* // Getting access to the service instance
* const babelConfiguration = container.get('babelConfiguration');
* @type {Provider}
*/
const babelConfiguration = provider((app) => {
app.set('babelConfiguration', () => new BabelConfiguration(
app.get('events')
));
});
module.exports = {
BabelConfiguration,
babelConfiguration,
};