@ui5/project
Version:
UI5 CLI - Project
488 lines (445 loc) • 18.3 kB
JavaScript
import {getLogger} from "@ui5/logger";
import composeTaskList from "./helpers/composeTaskList.js";
import {createReaderCollection} from "@ui5/fs/resourceFactory";
/**
* TaskRunner
*
* @private
* @hideconstructor
*/
class TaskRunner {
/**
* Constructor
*
* @param {object} parameters
* @param {object} parameters.graph
* @param {object} parameters.project
* @param {@ui5/logger/loggers/ProjectBuild} parameters.log Logger to use
* @param {@ui5/project/build/helpers/TaskUtil} parameters.taskUtil TaskUtil instance
* @param {@ui5/builder/tasks/taskRepository} parameters.taskRepository Task repository
* @param {@ui5/project/build/ProjectBuilder~BuildConfiguration} parameters.buildConfig
* Build configuration
*/
constructor({graph, project, log, taskUtil, taskRepository, buildConfig}) {
if (!graph || !project || !log || !taskUtil || !taskRepository || !buildConfig) {
throw new Error("TaskRunner: One or more mandatory parameters not provided");
}
this._project = project;
this._graph = graph;
this._taskUtil = taskUtil;
this._taskRepository = taskRepository;
this._buildConfig = buildConfig;
this._log = log;
this._directDependencies = new Set(this._taskUtil.getDependencies());
}
async _initTasks() {
if (this._tasks) {
return;
}
this._tasks = Object.create(null);
this._taskExecutionOrder = [];
const project = this._project;
let buildDefinition;
switch (project.getType()) {
case "application":
buildDefinition = "./definitions/application.js";
break;
case "library":
buildDefinition = "./definitions/library.js";
break;
case "module":
buildDefinition = "./definitions/module.js";
break;
case "theme-library":
buildDefinition = "./definitions/themeLibrary.js";
break;
default:
throw new Error(`Unknown project type ${project.getType()}`);
}
const {default: getStandardTasks} = await import(buildDefinition);
const standardTasks = getStandardTasks({
project,
taskUtil: this._taskUtil,
getTask: this._taskRepository.getTask
});
for (const [taskName, params] of standardTasks) {
this._addTask(taskName, params);
}
await this._addCustomTasks();
// Create readers for *all* dependencies
const depReaders = [];
await this._graph.traverseBreadthFirst(project.getName(), async function({project: dep}) {
if (dep.getName() === project.getName()) {
// Ignore project itself
return;
}
depReaders.push(dep.getReader());
});
this._allDependenciesReader = createReaderCollection({
name: `Dependency reader collection of project ${project.getName()}`,
readers: depReaders
});
}
/**
* Takes a list of tasks which should be executed from the available task list of the current builder
*
* @returns {Promise} Returns promise resolving once all tasks have been executed
*/
async runTasks() {
await this._initTasks();
const tasksToRun = composeTaskList(Object.keys(this._tasks), this._buildConfig);
const allTasks = this._taskExecutionOrder.filter((taskName) => {
// There might be a numeric suffix in case a custom task is configured multiple times.
// The suffix needs to be removed in order to check against the list of tasks to run.
//
// Note: The 'tasksToRun' parameter only allows to specify the custom task name
// (without suffix), so it executes either all or nothing.
// It's currently not possible to just execute some occurrences of a custom task.
// This would require a more robust contract to identify task executions
// (e.g. via an 'id' that can be assigned to a specific execution in the configuration).
const taskWithoutSuffixCounter = taskName.replace(/--\d+$/, "");
return tasksToRun.includes(taskWithoutSuffixCounter) &&
// Task can be explicitly excluded by making its taskFunction = null
this._tasks[taskName].task !== null;
});
this._log.setTasks(allTasks);
for (const taskName of allTasks) {
const taskFunction = this._tasks[taskName].task;
if (typeof taskFunction === "function") {
await this._executeTask(taskName, taskFunction);
}
}
}
/**
* First compiles a list of all tasks that will be executed, then a list of all direct project
* dependencies that those tasks require access to.
*
* @returns {Set<string>} Returns a set containing the names of all required direct project dependencies
*/
async getRequiredDependencies() {
await this._initTasks();
const tasksToRun = composeTaskList(Object.keys(this._tasks), this._buildConfig);
const allTasks = this._taskExecutionOrder.filter((taskName) => {
// There might be a numeric suffix in case a custom task is configured multiple times.
// The suffix needs to be removed in order to check against the list of tasks to run.
//
// Note: The 'tasksToRun' parameter only allows to specify the custom task name
// (without suffix), so it executes either all or nothing.
// It's currently not possible to just execute some occurrences of a custom task.
// This would require a more robust contract to identify task executions
// (e.g. via an 'id' that can be assigned to a specific execution in the configuration).
const taskWithoutSuffixCounter = taskName.replace(/--\d+$/, "");
return tasksToRun.includes(taskWithoutSuffixCounter);
});
return allTasks.reduce((requiredDependencies, taskName) => {
if (this._tasks[taskName].requiredDependencies.size) {
this._log.verbose(`Task ${taskName} for project ${this._project.getName()} requires dependencies`);
}
for (const depName of this._tasks[taskName].requiredDependencies) {
requiredDependencies.add(depName);
}
return requiredDependencies;
}, new Set());
}
/**
* Adds an executable task to the builder
*
* The order this function is being called defines the build order. FIFO.
*
* @param {string} taskName Name of the task which should be in the list availableTasks.
* @param {object} [parameters]
* @param {boolean} [parameters.requiresDependencies]
* @param {object} [parameters.options]
* @param {Function} [parameters.taskFunction]
*/
_addTask(taskName, {requiresDependencies = false, options = {}, taskFunction} = {}) {
if (this._tasks[taskName]) {
throw new Error(`Failed to add duplicate task ${taskName} for project ${this._project.getName()}`);
}
if (this._taskExecutionOrder.includes(taskName)) {
throw new Error(`Failed to add task ${taskName} for project ${this._project.getName()}. ` +
`It has already been scheduled for execution`);
}
let task;
if (taskFunction === null) {
this._log.verbose(`Task ${taskName} is set to be explicitly skipped in definitions.`);
task = null;
} else {
task = async (log) => {
options.projectName = this._project.getName();
options.projectNamespace = this._project.getNamespace();
const params = {
workspace: this._project.getWorkspace(),
taskUtil: this._taskUtil,
options
};
if (requiresDependencies) {
params.dependencies = this._allDependenciesReader;
}
if (!taskFunction) {
taskFunction = (await this._taskRepository.getTask(taskName)).task;
}
return taskFunction(params);
};
}
this._tasks[taskName] = {
task,
requiredDependencies: requiresDependencies ? this._directDependencies : new Set()
};
this._taskExecutionOrder.push(taskName);
}
/**
*
* @private
*/
async _addCustomTasks() {
const projectCustomTasks = this._project.getCustomTasks();
if (!projectCustomTasks || projectCustomTasks.length === 0) {
return; // No custom tasks defined
}
for (let i = 0; i < projectCustomTasks.length; i++) {
// Add tasks one-by-one to keep order as defined in project configuration
await this._addCustomTask(projectCustomTasks[i]);
}
}
/**
* Adds custom tasks to execute
*
* @private
* @param {object} taskDef
*/
async _addCustomTask(taskDef) {
const project = this._project;
const graph = this._graph;
const taskUtil = this._taskUtil;
if (!taskDef.name) {
throw new Error(`Missing name for custom task in configuration of project ${project.getName()}`);
}
if (taskDef.beforeTask && taskDef.afterTask) {
throw new Error(`Custom task definition ${taskDef.name} of project ${project.getName()} ` +
`defines both "beforeTask" and "afterTask" parameters. Only one must be defined.`);
}
if (this._taskExecutionOrder.length && !taskDef.beforeTask && !taskDef.afterTask) {
// Iff there are tasks configured, beforeTask or afterTask must be given
throw new Error(`Custom task definition ${taskDef.name} of project ${project.getName()} ` +
`defines neither a "beforeTask" nor an "afterTask" parameter. One must be defined.`);
}
const standardTasks = this._taskRepository.getAllTaskNames();
if (standardTasks.includes(taskDef.name)) {
throw new Error(
`Custom task configuration of project ${project.getName()} ` +
`references standard task ${taskDef.name}. Only custom tasks must be provided here.`);
}
let newTaskName = taskDef.name;
if (this._tasks[newTaskName]) {
// Task is already known
// => add a suffix to allow for multiple configurations of the same task
let suffixCounter = 1;
while (this._tasks[newTaskName]) {
suffixCounter++; // Start at 2
newTaskName = `${taskDef.name}--${suffixCounter}`;
}
}
const task = graph.getExtension(taskDef.name);
if (!task) {
throw new Error(
`Could not find custom task ${taskDef.name}, referenced by project ${project.getName()} ` +
`in project graph with root node ${graph.getRoot().getName()}`);
}
// Tasks can provide an optional callback to tell build process which dependencies they require
const requiredDependenciesCallback = await task.getRequiredDependenciesCallback();
const specVersion = task.getSpecVersion();
let requiredDependencies;
// Always provide a dependencies-reader, even if empty. Unless the task is specVersion >=3.0
// and did not define the respective callback.
// This is to distinguish between tasks semi-intentionally not requesting any dependencies,
// because none are available (i.e. because the project does not have any) and tasks that
// intentionally do not request any dependencies, by not providing a dependency-determination callback function
let provideDependenciesReader = true;
if (!requiredDependenciesCallback) {
if (specVersion.gte("3.0")) {
// Default for new spec versions: Provide no dependencies if no callback is provided
this._log.verbose(
`Custom task ${task.getName()} of project ${this._project.getName()} ` +
`does not provide a callback for determining its required dependencies. ` +
`Defaulting to not providing any dependencies to the task`);
requiredDependencies = new Set();
// Ensure that no reader is provided, in order to produce an exception if
// access is still attempted
provideDependenciesReader = false;
} else {
// Default for old spec versions: Assume all dependencies are required
requiredDependencies = this._directDependencies;
}
} else {
const dependencyDeterminationParams = {
availableDependencies: new Set(this._directDependencies)
};
if (specVersion.gte("3.0")) {
// Add getProjects, getDependencies and options to parameters
const taskUtilInterface = taskUtil.getInterface(specVersion);
dependencyDeterminationParams.getProject =
taskUtilInterface.getProject.bind(taskUtilInterface);
dependencyDeterminationParams.getDependencies =
taskUtilInterface.getDependencies.bind(taskUtilInterface);
}
dependencyDeterminationParams.options = {
projectName: project.getName(),
projectNamespace: project.getNamespace(),
configuration: taskDef.configuration,
taskName: newTaskName
};
requiredDependencies = await requiredDependenciesCallback(dependencyDeterminationParams);
if (!(requiredDependencies instanceof Set)) {
throw new Error(
`'determineRequiredDependencies' callback function of custom task ${task.getName()} of ` +
`project ${project.getName()} must resolve with Set.`);
}
requiredDependencies.forEach((depName) => {
// Returned requiredDependencies must be a subset of all direct dependencies of the project
if (!this._directDependencies.has(depName)) {
throw new Error(
`'determineRequiredDependencies' callback function of custom task ${task.getName()} ` +
`of project ${project.getName()} must resolve with a subset of the the direct ` +
`dependencies of the project. ${depName} is not a direct dependency of the project.`);
}
});
}
this._tasks[newTaskName] = {
task: this._createCustomTaskWrapper({
task,
project,
taskUtil,
taskName: newTaskName,
taskConfiguration: taskDef.configuration,
provideDependenciesReader,
getDependenciesReader: () => {
// Create the dependencies reader on-demand
return this._createDependenciesReader(requiredDependencies);
},
}),
requiredDependencies
};
if (this._taskExecutionOrder.length) {
// There is at least one task configured. Use before- and afterTask to add the custom task
const refTaskName = taskDef.beforeTask || taskDef.afterTask;
let refTaskIdx = this._taskExecutionOrder.indexOf(refTaskName);
if (refTaskIdx === -1) {
if (this._taskRepository.getRemovedTaskNames().includes(refTaskName)) {
throw new Error(
`Standard task ${refTaskName}, referenced by custom task ${newTaskName} ` +
`in project ${project.getName()}, ` +
`has been removed in this version of UI5 CLI and can't be referenced anymore. ` +
`Please see the migration guide at https://ui5.github.io/cli/updates/migrate-v3/`);
}
throw new Error(`Could not find task ${refTaskName}, referenced by custom task ${newTaskName}, ` +
`to be scheduled for project ${project.getName()}`);
}
if (taskDef.afterTask) {
// Insert after index of referenced task
refTaskIdx++;
}
this._taskExecutionOrder.splice(refTaskIdx, 0, newTaskName);
} else {
// There is no task configured so far. Just add the custom task
this._taskExecutionOrder.push(newTaskName);
}
}
_createCustomTaskWrapper({
project, taskUtil, getDependenciesReader, provideDependenciesReader, task, taskName, taskConfiguration
}) {
return async function() {
/* Custom Task Interface
Parameters:
{Object} parameters Parameters
{@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files
{@ui5/fs/AbstractReader} parameters.dependencies
Reader or Collection to read dependency files
{@ui5/project/build/helpers/TaskUtil} parameters.taskUtil Specification Version-dependent
interface of a [TaskUtil]{@link @ui5/project/build/helpers/TaskUtil} instance
{@ui5/logger/Logger} [parameters.log] Logger instance to use by the custom task.
This parameter is only available to custom task extensions defining
<b>Specification Version 3.0 and above</b>.
{Object} parameters.options Options
{string} parameters.options.projectName Project name
{string|null} parameters.options.projectNamespace Project namespace if available
{string} [parameters.options.taskName] Runtime name of the task.
If a task is executed multiple times, a suffix is added to distinguish the executions.
This attribute is only available to custom task extensions defining
<b>Specification Version 3.0 and above</b>.
{string} [parameters.options.configuration] Task configuration if given in ui5.yaml
Returns:
{Promise<undefined>} Promise resolving with undefined once data has been written
*/
const params = {
workspace: project.getWorkspace(),
options: {
projectName: project.getName(),
projectNamespace: project.getNamespace(),
configuration: taskConfiguration,
}
};
const specVersion = task.getSpecVersion();
const taskUtilInterface = taskUtil.getInterface(specVersion);
// Interface is undefined if specVersion does not support taskUtil
if (taskUtilInterface) {
params.taskUtil = taskUtilInterface;
}
const taskFunction = await task.getTask();
if (specVersion.gte("3.0")) {
params.options.taskName = taskName;
params.log = getLogger(`builder:custom-task:${taskName}`);
}
if (provideDependenciesReader) {
params.dependencies = await getDependenciesReader();
}
return taskFunction(params);
};
}
/**
* Adds progress related functionality to task function.
*
* @private
* @param {string} taskName Name of the task
* @param {Function} taskFunction Function which executed the task
* @param {object} taskParams Base parameters for all tasks
* @returns {Promise} Resolves when task has finished
*/
async _executeTask(taskName, taskFunction, taskParams) {
this._log.startTask(taskName);
this._taskStart = performance.now();
await taskFunction(taskParams, this._log);
if (this._log.isLevelEnabled("perf")) {
this._log.perf(`Task ${taskName} finished in ${Math.round((performance.now() - this._taskStart))} ms`);
}
this._log.endTask(taskName);
}
async _createDependenciesReader(requiredDirectDependencies) {
if (requiredDirectDependencies.size === this._directDependencies.size) {
// Shortcut: If all direct dependencies are required, just return the already created reader
return this._allDependenciesReader;
}
const rootProject = this._project;
// Collect readers for all requested dependencies
const readers = [];
// Add transitive dependencies to set of required dependencies
const requiredDependencies = new Set(requiredDirectDependencies);
for (const projectName of requiredDirectDependencies) {
this._graph.getTransitiveDependencies(projectName).forEach((depName) => {
requiredDependencies.add(depName);
});
}
// Collect readers for all (transitive) dependencies
await this._graph.traverseBreadthFirst(rootProject.getName(), async ({project}) => {
if (requiredDependencies.has(project.getName())) {
readers.push(project.getReader());
}
});
// Create a reader collection for that
return createReaderCollection({
name: `Reduced dependency reader collection of project ${rootProject.getName()}`,
readers
});
}
}
export default TaskRunner;