src/services/cli/cliSHBuild.js
const { provider } = require('jimple');
const CLICommand = require('../../abstracts/cliCommand');
/**
* This is the _'real build command'_. This is a private command the shell script executes in order
* to get a list of commands to run.
* @extends {CLICommand}
* @todo This whole class needs a refactor (args and params are the same!).
*/
class CLISHBuildCommand extends CLICommand {
/**
* Class constructor.
* @param {Builder} builder Needed to generate a target
* build command.
* @param {CLICleanCommand} cliCleanCommand Needed to generate the command
* that cleans a target files.
* @param {CLICopyProjectFilesCommand} cliCopyProjectFilesCommand Needed to generate the command
* to copy the project files if
* the feature of copying on
* build is enabled.
* @param {CLIRevisionCommand} cliRevisionCommand Needed to generate the command
* that creates the revision file
* if the feature of generating
* it on build is enabled.
* @param {CLISHCopyCommand} cliSHCopyCommand Needed to generate the command
* to copy the target files if
* the target doesn't require
* bundling.
* @param {CLISHNodeRunCommand} cliSHNodeRunCommand Needed to generate the command
* to run a Node target if the
* `run` option is used.
* @param {CLISHNodeWatchCommand} cliSHNodeWatchCommand Needed to generate the command
* to watch a Node target files
* if the `watch` option is used.
* @param {CLISHTranspileCommand} cliSHTranspileCommand Needed to generate the command
* to transpile a Node target
* code.
* @param {Events} events Used to reduce the list of
* commands generated.
* @param {ProjectConfigurationSettings} projectConfiguration Used to read and validate the
* features.
* @param {Targets} targets Used to get the targets
* information.
*/
constructor(
builder,
buildTypeScriptHelper,
cliCleanCommand,
cliCopyProjectFilesCommand,
cliRevisionCommand,
cliSHCopyCommand,
cliSHNodeRunCommand,
cliSHNodeWatchCommand,
cliSHTranspileCommand,
events,
projectConfiguration,
targets
) {
super();
/**
* A local reference for the `builder` service.
* @type {Builder}
*/
this.builder = builder;
/**
* A local reference for the `buildTypeScriptHelper` service.
* @type {BuildTypeScriptHelper}
*/
this.buildTypeScriptHelper = buildTypeScriptHelper;
/**
* A local reference for the `cliCleanCommand` service.
* @type {CliCleanCommand}
*/
this.cliCleanCommand = cliCleanCommand;
/**
* A local reference for the `cliCopyProjectFilesCommand` service.
* @type {CliCopyProjectFilesCommand}
*/
this.cliCopyProjectFilesCommand = cliCopyProjectFilesCommand;
/**
* A local reference for the `cliRevisionCommand` service.
* @type {CliRevisionCommand}
*/
this.cliRevisionCommand = cliRevisionCommand;
/**
* A local reference for the `cliSHCopyCommand` service.
* @type {CliSHCopyCommand}
*/
this.cliSHCopyCommand = cliSHCopyCommand;
/**
* A local reference for the `cliSHNodeRunCommand` service.
* @type {CliSHNodeRunCommand}
*/
this.cliSHNodeRunCommand = cliSHNodeRunCommand;
/**
* A local reference for the `cliSHNodeWatchCommand` service.
* @type {CliSHNodeWatchCommand}
*/
this.cliSHNodeWatchCommand = cliSHNodeWatchCommand;
/**
* A local reference for the `cliSHTranspileCommand` service.
* @type {CliSHTranspileCommand}
*/
this.cliSHTranspileCommand = cliSHTranspileCommand;
/**
* A local reference for the `events` service.
* @type {Events}
*/
this.events = events;
/**
* All the project settings.
* @type {ProjectConfigurationSettings}
*/
this.projectConfiguration = projectConfiguration;
/**
* A local reference for the `targets` service.
* @type {Targets}
*/
this.targets = targets;
/**
* The instruction needed to trigger the command.
* @type {string}
*/
this.command = 'sh-build [target]';
/**
* A description of the command, just to follow the interface as the command won't show up on
* the help interface.
* @type {string}
*/
this.description = 'Get the build commands for the shell program to execute';
this.addOption(
'type',
'-t, --type [type]',
'Which build type: development (default) or production',
'development'
);
this.addOption(
'run',
'-r, --run',
'Run the target after the build is completed. It only works when the ' +
'build type is development',
false
);
this.addOption(
'watch',
'-w, --watch',
'Rebuild the target every time one of its files changes. It only works ' +
'when the build type is development',
false
);
this.addOption(
'inspect',
'-i, --inspect',
'Enables the Node inspector. It only works when running Node targets',
false
);
this.addOption(
'analyze',
'-a, --analyze',
'Enables the bundle analyzer. It only works with targets with bundling',
false
);
/**
* Hide the command from the help interface.
* @type {boolean}
*/
this.hidden = true;
/**
* Enable unknown options so other services can customize the build command.
* @type {boolean}
*/
this.allowUnknownOptions = true;
}
/**
* Handle the execution of the command and outputs the list of commands to run.
* This method emits the event reducer `build-target-commands-list` with the list of commands,
* the target information, the type of build and whether or not the target should be executed;
* and it expects a list of commands on return.
* @param {?string} name The name of the target.
* @param {Command} command The executed command (sent by `commander`).
* @param {CLIBuildCommandOptions} options The command options.
* @param {Object} unknownOptions A dictionary of extra options that command may
* have received.
*/
handle(name, command, options, unknownOptions) {
const { type } = options;
// Get the target information
const target = name ?
// If the target doesn't exist, this will throw an error.
this.targets.getTarget(name) :
// Get the default target or throw an error if the project doesn't have targets.
this.targets.getDefaultTarget();
const {
development,
analyze,
run,
inspect,
watch,
} = this._normalizeOptions(options, target);
/**
* Check whether or not a build will be created. This is always `true` for browser targets, but
* it can be `false` for Node targets if bundling and transpiling is disabled.
*/
let build = true;
if (target.is.node) {
build = !development || target.bundle || target.transpile;
}
// Define the parameters object to send to the other methods.
const params = {
target,
type,
run,
build,
watch,
inspect,
analyze,
};
// Based on the target type, get the list of commands.
const commands = target.is.node ?
this._getCommandsForNodeTarget(params) :
this._getCommandsForBrowserTarget(params);
// Reduce the list of commands.
const output = this.events.reduce(
'build-target-commands-list',
commands.filter((cmd) => !!cmd),
params,
unknownOptions
)
// Join the commands on a single string.
.join(';');
// Outputs all the commands
this.output(output);
}
/**
* Normalizes the options received by the command in order to resolve "impossible combinations",
* like trying to analyze a target that is not for bundling or trying to inspect a browser
* target.
* @param {CLIBuildCommandOptions} options The command options.
* @param {Target} target The target information.
* @return {CLIBuildCommandNormalizedOptions}
* @access protected
* @ignore
*/
_normalizeOptions(options, target) {
const development = options.type === 'development';
// Check if there's a reason to analyze the target bundle.
const analyze = options.analyze && (target.is.browser || target.bundle);
// Check if there's a reason for the target to be executed.
const run = !analyze && development && (target.runOnDevelopment || options.run);
// Check if there's a reason for the Node inspector to be enabled.
const inspect = run && target.is.node && (target.inspect.enabled || options.inspect);
// Check if the target files should be watched.
const watch = !run && (target.watch[options.type] || options.watch);
return {
development,
analyze,
run,
inspect,
watch,
};
}
/**
* Get the build (and run) commands for a Node target.
* @param {CLIBuildCommandParams} params A dictionary with all the required information the
* service needs to run the command: The target
* information, the build type, whether or not the target
* will be executed, etc.
* @return {Array}
* @access protected
* @ignore
*/
_getCommandsForNodeTarget(params) {
// Get the base commands.
const commands = [
this._getCleanCommandIfNeeded(params),
this._getBuildCommandIfNeeded(params),
this._getCopyCommandIfNeeded(params),
this._getTranspileCommandIfNeeded(params),
this._getTypeScriptDeclarationsCommandIfNeeded(params),
];
// If the target won't be executed nor their files will be watched...
if (!params.run && !params.watch) {
// ...push the commands to create the revision file and copy the project files.
commands.push(...[
this._getRevisionCommandIfNeeded(params),
this._getCopyProjectFilesCommand(params),
]);
} else if (!params.target.bundle) {
/**
* If the target will be executed or their files will be watched, and is not a bundled target,
* push the command to either run or watch. The reason it's handled this ways is because if
* the target is bundled, the build engine will take care of the execution/watch.
*/
if (params.run) {
// Run the target with `nodemon`.
commands.push(this._getNodeRunCommand(params));
} else if (params.type === 'production' || params.target.transpile) {
// Watch the target with `watchpack`.
commands.push(this._getNodeWatchCommand(params));
}
}
return commands;
}
/**
* Get the build (and run) commands for a browser target.
* @param {CLIBuildCommandParams} params A dictionary with all the required information the
* service needs to run the command: The target
* information, the build type, whether or not the target
* will be executed, etc.
* @return {Array}
* @access protected
* @ignore
*/
_getCommandsForBrowserTarget(params) {
// Get the base commands.
const commands = [
this._getCleanCommandIfNeeded(params),
this._getBuildCommandIfNeeded(params),
this._getTypeScriptDeclarationsCommandIfNeeded(params),
];
// If the target won't be executed...
if (!params.run && !params.watch) {
// ...push the commands to create the revision file and copy the project files.
commands.push(...[
this._getRevisionCommandIfNeeded(params),
this._getCopyProjectFilesCommand(params),
]);
}
return commands;
}
/**
* Get the command to remove the previous build files of a target, but only if the target will be
* build, otherwise, it will return an empty string.
* @param {CLIBuildCommandParams} params A dictionary with all the required information the
* service needs to run the command: The target
* information, the build type, whether or not the target
* will be executed, etc.
* @return {string}
* @access protected
* @ignore
*/
_getCleanCommandIfNeeded(params) {
let command = '';
if (params.build && params.target.cleanBeforeBuild) {
command = this.cliCleanCommand.generate({
target: params.target.name,
});
}
return command;
}
/**
* Get the command to actually build a target.
* @param {CLIBuildCommandParams} params A dictionary with all the required information the
* service needs to run the command: The target
* information, the build type, whether or not the target
* will be executed, etc.
* @return {string}
* @access protected
* @ignore
*/
_getBuildCommandIfNeeded(params) {
return this.builder.getTargetBuildCommand(
params.target,
params.type,
params.run,
params.watch,
params.inspect,
params.analyze
);
}
/**
* Get the command to copy a target files, but only if the target will be _'build'_ (transpiled
* counts) and it doesn't support bundling, otherwise, it will return an empty string.
* @param {CLIBuildCommandParams} params A dictionary with all the required information the
* service needs to run the command: The target
* information, the build type, whether or not the target
* will be executed, etc.
* @return {string}
* @access protected
* @ignore
*/
_getCopyCommandIfNeeded(params) {
let command = '';
if (params.build && !params.target.bundle) {
command = this.cliSHCopyCommand.generate({
target: params.target.name,
type: params.type,
});
}
return command;
}
/**
* Get the command to transpile a target files, but only if the target will be _'build'_
* (transpiled counts) and it doesn't support bundling, otherwise, it will return an empty string.
* @param {CLIBuildCommandParams} params A dictionary with all the required information the
* service needs to run the command: The target
* information, the build type, whether or not the target
* will be executed, etc.
* @return {string}
* @access protected
* @ignore
*/
_getTranspileCommandIfNeeded(params) {
let command = '';
if (params.build && !params.target.bundle) {
command = this.cliSHTranspileCommand.generate({
target: params.target.name,
type: params.type,
});
}
return command;
}
/**
* Get the command to generate a TypeScript target type declarations, but only if the target
* uses TypeScript, won't run and won't be watched: The idea is to generate the declarations only
* when you build the target and not during all the development process.
* @param {CLIBuildCommandParams} params A dictionary with all the required information the
* service needs to run the command: The target
* information, the build type, whether or not the target
* will be executed, etc.
* @return {string}
* @access protected
* @ignore
*/
_getTypeScriptDeclarationsCommandIfNeeded(params) {
let command = '';
if (
params.target.typeScript &&
params.build &&
!params.run &&
!params.watch
) {
command = this.buildTypeScriptHelper.getDeclarationsCommand(params.target);
}
return command;
}
/**
* Get the command to run a Node target.
* @param {CLIBuildCommandParams} params A dictionary with all the required information the
* service needs to run the command: The target
* information, the build type, whether or not the target
* will be executed, etc.
* @return {string}
* @access protected
* @ignore
*/
_getNodeRunCommand(params) {
return this.cliSHNodeRunCommand.generate({
target: params.target.name,
inspect: params.inspect,
});
}
/**
* Get the command to watch a Node target.
* @param {CLIBuildCommandParams} params A dictionary with all the required information the
* service needs to run the command: The target
* information, the build type, whether or not the target
* will be executed, etc.
* @return {string}
* @access protected
* @ignore
*/
_getNodeWatchCommand(params) {
return this.cliSHNodeWatchCommand.generate({
target: params.target.name,
});
}
/**
* Get the command to create the revision file, but only if the feature is enabled, otherwise,
* it will return an empty string.
* @param {CLIBuildCommandParams} params A dictionary with all the required information the
* service needs to run the command: The target
* information, the build type, whether or not the target
* will be executed, etc.
* @return {string}
* @access protected
* @ignore
*/
_getRevisionCommandIfNeeded(params) {
const {
enabled,
createRevisionOnBuild,
} = this.projectConfiguration.version.revision;
let command = '';
if (enabled && createRevisionOnBuild.enabled) {
const revisionEnvCheck = !createRevisionOnBuild.onlyOnProduction ||
(createRevisionOnBuild.onlyOnProduction && params.type === 'production');
const revisionTargetCheck = !createRevisionOnBuild.targets.length ||
createRevisionOnBuild.targets.includes(params.target.name);
if (revisionEnvCheck && revisionTargetCheck) {
command = this.cliRevisionCommand.generate();
}
}
return command;
}
/**
* Get the command to copy the project files, but only if the feature is enabled, otherwise,
* it will return an empty string.
* @param {CLIBuildCommandParams} params A dictionary with all the required information the
* service needs to run the command: The target
* information, the build type, whether or not the target
* will be executed, etc.
* @return {string}
* @access protected
* @ignore
*/
_getCopyProjectFilesCommand(params) {
const { enabled, copyOnBuild } = this.projectConfiguration.copy;
let command = '';
if (enabled && copyOnBuild.enabled) {
const copyEnvCheck = !copyOnBuild.onlyOnProduction ||
(copyOnBuild.onlyOnProduction && params.type === 'production');
const copyTargetCheck = !copyOnBuild.targets.length ||
copyOnBuild.targets.includes(params.target.name);
if (copyEnvCheck && copyTargetCheck) {
command = this.cliCopyProjectFilesCommand.generate();
}
}
return command;
}
}
/**
* The service provider that once registered on the app container will set an instance of
* `CLISHBuildCommand` as the `cliSHBuildCommand` service.
* @example
* // Register it on the container
* container.register(cliSHBuildCommand);
* // Getting access to the service instance
* const cliSHBuildCommand = container.get('cliSHBuildCommand');
* @type {Provider}
*/
const cliSHBuildCommand = provider((app) => {
app.set('cliSHBuildCommand', () => new CLISHBuildCommand(
app.get('builder'),
app.get('buildTypeScriptHelper'),
app.get('cliCleanCommand'),
app.get('cliCopyProjectFilesCommand'),
app.get('cliRevisionCommand'),
app.get('cliSHCopyCommand'),
app.get('cliSHNodeRunCommand'),
app.get('cliSHNodeWatchCommand'),
app.get('cliSHTranspileCommand'),
app.get('events'),
app.get('projectConfiguration').getConfig(),
app.get('targets')
));
});
module.exports = {
CLISHBuildCommand,
cliSHBuildCommand,
};