src/services/building/configuration.js
const path = require('path');
const extend = require('extend');
const { provider } = require('jimple');
/**
* This service reads the targets information and generates what would be the contents of a
* `webpack.config.js` for them.
*/
class WebpackConfiguration {
/**
* Class constructor.
* @param {BuildVersion} buildVersion To load the project version.
* @param {PathUtils} pathUtils To generate the Webpack paths.
* @param {Targets} targets To get the target information.
* @param {TargetsFileRules} targetsFileRules To get the file rules of the target.
* @param {TargetConfigurationCreator} targetConfiguration To create an overwrite
* configuration for the target.
* @param {WebpackConfigurations} webpackConfigurations A dictionary of configurations
* for target type and build type.
*/
constructor(
buildVersion,
pathUtils,
targets,
targetsFileRules,
targetConfiguration,
webpackConfigurations
) {
/**
* A local reference for the `buildVersion` service.
* @type {BuildVersion}
*/
this.buildVersion = buildVersion;
/**
* A local reference for the `pathUtils` service.
* @type {PathUtils}
*/
this.pathUtils = pathUtils;
/**
* A local reference for the `targets` service.
* @type {Targets}
*/
this.targets = targets;
/**
* A local reference for the `targetsFileRules` service.
* @type {TargetsFileRules}
*/
this.targetsFileRules = targetsFileRules;
/**
* A local reference for the `targetConfiguration` function service.
* @type {TargetConfigurationCreator}
*/
this.targetConfiguration = targetConfiguration;
/**
* A dictionary with the configurations for target type and build type.
* @type {WebpackConfigurations}
*/
this.webpackConfigurations = webpackConfigurations;
}
/**
* This method generates a complete webpack configuration for a target.
* Before creating the configuration, it uses the reducer event
* `webpack-configuration-parameters-for-browser` or `webpack-configuration-parameters-for-node`,
* depending on the target type, and then `webpack-configuration-parameters` to reduce
* the parameters ({@link WebpackConfigurationParams}) the services will use to generate the
* configuration. The event recevies the parameters and expects updated parameters in return.
* @param {Target} target The target information.
* @param {string} buildType The intended build type: `production` or `development`.
* @return {Object}
* @throws {Error} If there's no base configuration for the target type.
* @throws {Error} If there's no base configuration for the target type and build type.
* @todo Stop using `events` from `targets` and inject it directly on the class.
*/
getConfig(target, buildType) {
const targetType = target.type;
if (!this.webpackConfigurations[targetType]) {
throw new Error(`There's no configuration for the selected target type: ${targetType}`);
} else if (!this.webpackConfigurations[targetType][buildType]) {
throw new Error(`There's no configuration for the selected build type: ${buildType}`);
}
const copy = [];
if (target.is.browser || target.bundle) {
copy.push(...this.targets.getFilesToCopy(target, buildType));
}
const output = Object.assign({}, target.output[buildType]);
if (typeof output.jsChunks !== 'string') {
output.jsChunks = this._generateChunkName(output.js);
}
const definitions = this._getDefinitionsGenerator(target, buildType);
const additionalWatch = this._getBrowserTargetConfigurationDefinitions(target).files;
let params = {
target,
targetRules: this.targetsFileRules.getRulesForTarget(target),
entry: {
[target.name]: [path.join(target.paths.source, target.entry[buildType])],
},
definitions,
output,
copy,
buildType,
additionalWatch,
/**
* The reason we are taking this property is because it's not part of the `Target` entity,
* but it may be injected by the build engine.
*/
analyze: !!target.analyze,
};
const eventName = params.target.is.node ?
'webpack-configuration-parameters-for-node' :
'webpack-configuration-parameters-for-browser';
params = this.targets.events.reduce(
[eventName, 'webpack-configuration-parameters'],
params
);
let config = this.targetConfiguration(
`webpack/${target.name}.config.js`,
this.webpackConfigurations[targetType][buildType]
);
config = this.targetConfiguration(
`webpack/${target.name}.${buildType}.config.js`,
config
).getConfig(params);
config.output.path = this.pathUtils.join(config.output.path);
if (target.library) {
config.output = extend(true, {}, config.output, this._getLibraryOptions(target));
}
return config;
}
/**
* Generates a function that when called will return a dictionary with definitions that will be
* replaced on the bundle.
* @param {Target} target The target information.
* @param {string} buildType The intended build type: `production` or `development`.
* @return {Function():Object}
* @access protected
* @ignore
*/
_getDefinitionsGenerator(target, buildType) {
return () => this._getTargetDefinitions(target, buildType);
}
/**
* Generates a dictionary with definitions that will be replaced on the bundle. These
* definitions are things like `process.env.NODE_ENV`, the bundle version, a browser target
* configuration, etc.
* @param {Target} target The target information.
* @param {string} buildType The intended build type: `production` or `development`.
* @return {Object}
* @access protected
* @ignore
*/
_getTargetDefinitions(target, buildType) {
const targetVariables = this.targets.loadTargetDotEnvFile(target, buildType);
const definitions = Object.keys(targetVariables).reduce(
(current, variableName) => Object.assign({}, current, {
[`process.env.${variableName}`]: JSON.stringify(targetVariables[variableName]),
}),
{}
);
definitions['process.env.NODE_ENV'] = `'${buildType}'`;
definitions[this.buildVersion.getDefinitionVariable()] = JSON.stringify(
this.buildVersion.getVersion()
);
return Object.assign(
{},
definitions,
this._getBrowserTargetConfigurationDefinitions(target).definitions
);
}
/**
* This is a wrapper on top of {@link Targets#getBrowserTargetConfiguration} so no matter the
* type of target it recevies, or if the feature is disabled, it will always return the same
* signature.
* It also takes care of formatting the configuration on a "definitions object" so it can be
* added to the rest of the targets definitions.
* @param {Target} target The target information.
* @return {Object}
* @property {Object} definitions A dictionary with
* @property {Array} files The list of files involved on the configuration creation.
* @access protected
* @ignore
*/
_getBrowserTargetConfigurationDefinitions(target) {
let result;
if (target.is.browser && target.configuration && target.configuration.enabled) {
const parsed = this.targets.getBrowserTargetConfiguration(target);
result = {
definitions: {
[target.configuration.defineOn]: JSON.stringify(parsed.configuration),
},
files: parsed.files,
};
} else {
result = {
definitions: {},
files: [],
};
}
return result;
}
/**
* In case the target is a library, this method will be called in order to get the extra output
* settings webpack needs.
* @param {Target} target The target information.
* @return {Object}
* @access protected
* @ignore
*/
_getLibraryOptions(target) {
const { libraryOptions } = target;
// Create the object for webpack.
const newOptions = Object.assign({
libraryTarget: 'commonjs2',
}, libraryOptions);
// Remove any option unsupported by the webpack schema
[
'compress',
].forEach((invalidOption) => {
delete newOptions[invalidOption];
});
return newOptions;
}
/**
* This is a small helper function that parses the default path of the JS file webpack will
* emmit and adds a `[name]` placeholder for webpack to replace with the chunk name.
* @param {string} jsPath The original path for the JS file.
* @return {string}
* @access protected
* @ignore
*/
_generateChunkName(jsPath) {
const parsed = path.parse(jsPath);
return path.join(parsed.dir, `${parsed.name}.[name]${parsed.ext}`);
}
}
/**
* The service provider that once registered on the app container will set an instance of
* `WebpackConfiguration` as the `webpackConfiguration` service.
* @example
* // Register it on the container
* container.register(webpackConfiguration);
* // Getting access to the service instance
* const webpackConfiguration = container.get('webpackConfiguration');
* @type {Provider}
*/
const webpackConfiguration = provider((app) => {
app.set('webpackConfiguration', () => {
const webpackConfigurations = {
node: {
development: app.get('webpackNodeDevelopmentConfiguration'),
production: app.get('webpackNodeProductionConfiguration'),
},
browser: {
development: app.get('webpackBrowserDevelopmentConfiguration'),
production: app.get('webpackBrowserProductionConfiguration'),
},
};
return new WebpackConfiguration(
app.get('buildVersion'),
app.get('pathUtils'),
app.get('targets'),
app.get('targetsFileRules'),
app.get('targetConfiguration'),
webpackConfigurations
);
});
});
module.exports = {
WebpackConfiguration,
webpackConfiguration,
};