src/services/building/configuration.js
const path = require('path');
const { provider } = require('jimple');
/**
* This service reads the targets information and generates what would be the contents of a
* Rollup configuration file for them.
*/
class RollupConfiguration {
/**
* Class constructor.
* @param {BuildVersion} buildVersion To load the project version.
* @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 {RollupConfigurations} rollupConfigurations A dictionary of configurations
* for target type and build type.
*/
constructor(
buildVersion,
targets,
targetsFileRules,
targetConfiguration,
rollupConfigurations
) {
/**
* A local reference for the `buildVersion` service.
* @type {BuildVersion}
*/
this.buildVersion = buildVersion;
/**
* 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 {RollupConfigurations}
*/
this.rollupConfigurations = rollupConfigurations;
}
/**
* This method generates a complete Rollup configuration for a target.
* Before creating the configuration, it uses the reducer event
* `rollup-configuration-parameters-for-browser` or `rollup-configuration-parameters-for-node`,
* depending on the target type, and then `rollup-configuration-parameters` to reduce
* the parameters ({@link RollupConfigurationParams}) 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.rollupConfigurations[targetType]) {
throw new Error(`There's no configuration for the selected target type: ${targetType}`);
} else if (!this.rollupConfigurations[targetType][buildType]) {
throw new Error(`There's no configuration for the selected build type: ${buildType}`);
}
const input = path.join(target.paths.source, target.entry[buildType]);
const paths = Object.assign({}, target.output[buildType]);
if (paths.jsChunks === true) {
paths.jsChunks = this._generateChunkName(paths.js);
}
const output = this._getTargetOutput(target, paths, buildType);
const copy = [];
if (target.is.browser || target.bundle) {
copy.push(...this.targets.getFilesToCopy(target, buildType));
}
const definitions = this._getDefinitionsGenerator(target, buildType);
const additionalWatch = this._getBrowserTargetConfigurationDefinitions(target).files;
let params = {
input,
output,
target,
targetRules: this.targetsFileRules.getRulesForTarget(target),
definitions,
buildType,
paths,
copy,
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 = target.is.node ?
'rollup-configuration-parameters-for-node' :
'rollup-configuration-parameters-for-browser';
params = this.targets.events.reduce(
[eventName, 'rollup-configuration-parameters'],
params
);
let config = this.targetConfiguration(
`rollup/${target.name}.config.js`,
this.rollupConfigurations[targetType][buildType]
);
config = this.targetConfiguration(
`rollup/${target.name}.${buildType}.config.js`,
config
).getConfig(params);
return config;
}
/**
* Generates the Rollup output configuration setting based on the target information, its
* pared paths and the type of build.
* @param {Target} target The target information.
* @param {Object} formattedPaths The target `paths` setting for the selected build type. The
* reason they are received as a separated parameter is because
* in case the paths originally had a `jsChunk` property, the
* service parsed it in order to inject the paths to the
* actual chunks. Check the method `getConfig` for more
* information.
* @param {string} buildType The intended build type: `production` or `development`.
* @return {Object} The Rollup output configuration.
* @property {boolean} sourcemap Whether or not to include source maps.
* @property {string} name The name of the bundle, in case it exports something.
* @property {string} format The bundle format (`es`, `iifee` or `cjs`).
* @property {string} file The name of the bundle when code splitting is not used.
* @property {?string} exports In case the target is a library, this will `named`, as the
* default export mode for libraries.
* @property {?string} chunkFileNames If code splitting is used, this will be the base name of
* the chunk files.
* @property {?string} entryFileNames If code splitting is used, this will be the base name of
* the main bundle.
* @property {?string} dir If code splitting is used, this will be the directory
* where the chunk files will be saved.
* @access protected
* @ignore
*/
_getTargetOutput(target, formattedPaths, buildType) {
const output = {
sourcemap: !!(target.sourceMap && target.sourceMap[buildType]),
name: target.name.replace(/-(\w)/ig, (match, letter) => letter.toUpperCase()),
};
if (target.library) {
output.format = this._getLibraryFormat(target.libraryOptions);
output.exports = 'named';
} else {
output.format = this._getTargetDefaultFormat(target);
}
const filepath = `./${target.folders.build}/${formattedPaths.js}`;
if (formattedPaths.jsChunks) {
output.chunkFileNames = path.basename(formattedPaths.jsChunks);
output.entryFileNames = path.basename(formattedPaths.js);
output.dir = path.dirname(filepath);
if (target.is.browser && !target.library) {
output.format = 'es';
}
} else {
output.file = filepath;
}
return output;
}
/**
* Based on the taget type, this method will decide which will be the default output format
* the target will use. The reason this is the "default" format, it's because the service
* can later changed dependeding in whether the target is a library or not.
* @param {Target} target The target information.
* @return {string}
* @access protected
* @ignore
*/
_getTargetDefaultFormat(target) {
return target.is.node ? 'cjs' : 'iife';
}
/**
* 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;
}
/**
* Validate and format a target library format in order to make it work with Rollup supported
* types.
* @param {ProjectConfigurationNodeTargetTemplateLibraryOptions} options The target library
* options.
* @return {string}
* @access protected
* @ignore
*/
_getLibraryFormat(options) {
const format = options.libraryTarget.toLowerCase();
let result;
switch (format) {
case 'commonjs2':
result = 'cjs';
break;
case 'window':
case 'umd':
default:
result = 'umd';
}
return result;
}
/**
* This is a small helper function that parses the default path of the JS file Rollup will
* emmit and adds a `[name]` placeholder for Rollup 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
* `RollupConfiguration` as the `rollupConfiguration` service.
* @example
* // Register it on the container
* container.register(rollupConfiguration);
* // Getting access to the service instance
* const rollupConfiguration = container.get('rollupConfiguration');
* @type {Provider}
*/
const rollupConfiguration = provider((app) => {
app.set('rollupConfiguration', () => {
const rollupConfigurations = {
node: {
development: app.get('rollupNodeDevelopmentConfiguration'),
production: app.get('rollupNodeProductionConfiguration'),
},
browser: {
development: app.get('rollupBrowserDevelopmentConfiguration'),
production: app.get('rollupBrowserProductionConfiguration'),
},
};
return new RollupConfiguration(
app.get('buildVersion'),
app.get('targets'),
app.get('targetsFileRules'),
app.get('targetConfiguration'),
rollupConfigurations
);
});
});
module.exports = {
RollupConfiguration,
rollupConfiguration,
};