builder/lib/types/AbstractBuilder.js

  1. /**
  2. * Resource collections
  3. *
  4. * @public
  5. * @typedef module:@ui5/builder.BuilderResourceCollections
  6. * @property {module:@ui5/fs.DuplexCollection} workspace Workspace Resource
  7. * @property {module:@ui5/fs.ReaderCollection} dependencies Workspace Resource
  8. */
  9. /**
  10. * Base class for the builder implementation of a project type
  11. *
  12. * @abstract
  13. */
  14. class AbstractBuilder {
  15. /**
  16. * Constructor
  17. *
  18. * @param {object} parameters
  19. * @param {BuilderResourceCollections} parameters.resourceCollections Resource collections
  20. * @param {object} parameters.project Project configuration
  21. * @param {GroupLogger} parameters.parentLogger Logger to use
  22. * @param {object} parameters.taskUtil
  23. */
  24. constructor({resourceCollections, project, parentLogger, taskUtil}) {
  25. if (new.target === AbstractBuilder) {
  26. throw new TypeError("Class 'AbstractBuilder' is abstract");
  27. }
  28. this.project = project;
  29. this.log = parentLogger.createSubLogger(project.type + " " + project.metadata.name, 0.2);
  30. this.taskLog = this.log.createTaskLogger("🔨");
  31. this.tasks = {};
  32. this.taskExecutionOrder = [];
  33. this.addStandardTasks({
  34. resourceCollections,
  35. project,
  36. log: this.log,
  37. taskUtil
  38. });
  39. this.addCustomTasks({
  40. resourceCollections,
  41. project,
  42. taskUtil
  43. });
  44. }
  45. /**
  46. * Adds all standard tasks to execute
  47. *
  48. * @abstract
  49. * @protected
  50. * @param {object} parameters
  51. * @param {BuilderResourceCollections} parameters.resourceCollections Resource collections
  52. * @param {object} parameters.taskUtil
  53. * @param {object} parameters.project Project configuration
  54. * @param {object} parameters.log <code>@ui5/logger</code> logger instance
  55. */
  56. addStandardTasks({resourceCollections, project, log, taskUtil}) {
  57. throw new Error("Function 'addStandardTasks' is not implemented");
  58. }
  59. /**
  60. * Adds custom tasks to execute
  61. *
  62. * @private
  63. * @param {object} parameters
  64. * @param {BuilderResourceCollections} parameters.resourceCollections Resource collections
  65. * @param {object} parameters.taskUtil
  66. * @param {object} parameters.project Project configuration
  67. */
  68. addCustomTasks({resourceCollections, project, taskUtil}) {
  69. const projectCustomTasks = project.builder && project.builder.customTasks;
  70. if (!projectCustomTasks || projectCustomTasks.length === 0) {
  71. return; // No custom tasks defined
  72. }
  73. const taskRepository = require("../tasks/taskRepository");
  74. for (let i = 0; i < projectCustomTasks.length; i++) {
  75. const taskDef = projectCustomTasks[i];
  76. if (!taskDef.name) {
  77. throw new Error(`Missing name for custom task definition of project ${project.metadata.name} ` +
  78. `at index ${i}`);
  79. }
  80. if (taskDef.beforeTask && taskDef.afterTask) {
  81. throw new Error(`Custom task definition ${taskDef.name} of project ${project.metadata.name} ` +
  82. `defines both "beforeTask" and "afterTask" parameters. Only one must be defined.`);
  83. }
  84. if (this.taskExecutionOrder.length && !taskDef.beforeTask && !taskDef.afterTask) {
  85. // Iff there are tasks configured, beforeTask or afterTask must be given
  86. throw new Error(`Custom task definition ${taskDef.name} of project ${project.metadata.name} ` +
  87. `defines neither a "beforeTask" nor an "afterTask" parameter. One must be defined.`);
  88. }
  89. let newTaskName = taskDef.name;
  90. if (this.tasks[newTaskName]) {
  91. // Task is already known
  92. // => add a suffix to allow for multiple configurations of the same task
  93. let suffixCounter = 0;
  94. while (this.tasks[newTaskName]) {
  95. suffixCounter++; // Start at 1
  96. newTaskName = `${taskDef.name}--${suffixCounter}`;
  97. }
  98. }
  99. // Create custom task if not already done (task might be referenced multiple times, first one wins)
  100. const {specVersion, task} = taskRepository.getTask(taskDef.name);
  101. const execTask = function() {
  102. /* Custom Task Interface
  103. Parameters:
  104. {Object} parameters Parameters
  105. {module:@ui5/fs.DuplexCollection} parameters.workspace DuplexCollection to read and write files
  106. {module:@ui5/fs.AbstractReader} parameters.dependencies
  107. Reader or Collection to read dependency files
  108. {Object} parameters.taskUtil Specification Version dependent interface to a
  109. [TaskUtil]{@link module:@ui5/builder.tasks.TaskUtil} instance
  110. {Object} parameters.options Options
  111. {string} parameters.options.projectName Project name
  112. {string} [parameters.options.projectNamespace] Project namespace if available
  113. {string} [parameters.options.configuration] Task configuration if given in ui5.yaml
  114. Returns:
  115. {Promise<undefined>} Promise resolving with undefined once data has been written
  116. */
  117. const params = {
  118. workspace: resourceCollections.workspace,
  119. dependencies: resourceCollections.dependencies,
  120. options: {
  121. projectName: project.metadata.name,
  122. projectNamespace: project.metadata.namespace,
  123. configuration: taskDef.configuration
  124. }
  125. };
  126. const taskUtilInterface = taskUtil.getInterface(specVersion);
  127. // Interface is undefined if specVersion does not support taskUtil
  128. if (taskUtilInterface) {
  129. params.taskUtil = taskUtilInterface;
  130. }
  131. return task(params);
  132. };
  133. this.tasks[newTaskName] = execTask;
  134. if (this.taskExecutionOrder.length) {
  135. // There is at least one task configured. Use before- and afterTask to add the custom task
  136. const refTaskName = taskDef.beforeTask || taskDef.afterTask;
  137. let refTaskIdx = this.taskExecutionOrder.indexOf(refTaskName);
  138. if (refTaskIdx === -1) {
  139. throw new Error(`Could not find task ${refTaskName}, referenced by custom task ${newTaskName}, ` +
  140. `to be scheduled for project ${project.metadata.name}`);
  141. }
  142. if (taskDef.afterTask) {
  143. // Insert after index of referenced task
  144. refTaskIdx++;
  145. }
  146. this.taskExecutionOrder.splice(refTaskIdx, 0, newTaskName);
  147. } else {
  148. // There is no task configured so far. Just add the custom task
  149. this.taskExecutionOrder.push(newTaskName);
  150. }
  151. }
  152. }
  153. /**
  154. * Adds a executable task to the builder
  155. *
  156. * The order this function is being called defines the build order. FIFO.
  157. *
  158. * @param {string} taskName Name of the task which should be in the list availableTasks.
  159. * @param {Function} taskFunction
  160. */
  161. addTask(taskName, taskFunction) {
  162. if (this.tasks[taskName]) {
  163. throw new Error(`Failed to add duplicate task ${taskName} for project ${this.project.metadata.name}`);
  164. }
  165. if (this.taskExecutionOrder.includes(taskName)) {
  166. throw new Error(`Builder: Failed to add task ${taskName} for project ${this.project.metadata.name}. ` +
  167. `It has already been scheduled for execution.`);
  168. }
  169. this.tasks[taskName] = taskFunction;
  170. this.taskExecutionOrder.push(taskName);
  171. }
  172. /**
  173. * Check whether a task is defined
  174. *
  175. * @private
  176. * @param {string} taskName
  177. * @returns {boolean}
  178. */
  179. hasTask(taskName) {
  180. // TODO 3.0: Check whether this method is still required.
  181. // Only usage within #build seems to be unnecessary as all tasks are also added to the taskExecutionOrder
  182. return Object.prototype.hasOwnProperty.call(this.tasks, taskName);
  183. }
  184. /**
  185. * Takes a list of tasks which should be executed from the available task list of the current builder
  186. *
  187. * @param {Array} tasksToRun List of tasks which should be executed
  188. * @returns {Promise} Returns promise chain with tasks
  189. */
  190. build(tasksToRun) {
  191. const allTasks = this.taskExecutionOrder.filter((taskName) => {
  192. // There might be a numeric suffix in case a custom task is configured multiple times.
  193. // The suffix needs to be removed in order to check against the list of tasks to run.
  194. //
  195. // Note: The 'tasksToRun' parameter only allows to specify the custom task name
  196. // (without suffix), so it executes either all or nothing.
  197. // It's currently not possible to just execute some occurrences of a custom task.
  198. // This would require a more robust contract to identify task executions
  199. // (e.g. via an 'id' that can be assigned to a specific execution in the configuration).
  200. const taskWithoutSuffixCounter = taskName.replace(/--\d+$/, "");
  201. return this.hasTask(taskName) && tasksToRun.includes(taskWithoutSuffixCounter);
  202. });
  203. this.taskLog.addWork(allTasks.length);
  204. return allTasks.reduce((taskChain, taskName) => {
  205. const taskFunction = this.tasks[taskName];
  206. if (typeof taskFunction === "function") {
  207. taskChain = taskChain.then(this.wrapTask(taskName, taskFunction));
  208. }
  209. return taskChain;
  210. }, Promise.resolve());
  211. }
  212. /**
  213. * Adds progress related functionality to task function.
  214. *
  215. * @private
  216. * @param {string} taskName Name of the task
  217. * @param {Function} taskFunction Function which executed the task
  218. * @returns {Function} Wrapped task function
  219. */
  220. wrapTask(taskName, taskFunction) {
  221. return () => {
  222. this.taskLog.startWork(`Running task ${taskName}...`);
  223. return taskFunction().then(() => this.taskLog.completeWork(1));
  224. };
  225. }
  226. /**
  227. * Appends the list of 'excludes' to the list of 'patterns'. To harmonize both lists, the 'excludes'
  228. * are negated and the 'patternPrefix' is added to make them absolute.
  229. *
  230. * @private
  231. * @param {string[]} patterns
  232. * List of absolute default patterns.
  233. * @param {string[]} excludes
  234. * List of relative patterns to be excluded. Excludes with a leading "!" are meant to be re-included.
  235. * @param {string} patternPrefix
  236. * Prefix to be added to the excludes to make them absolute. The prefix must have a leading and a
  237. * trailing "/".
  238. */
  239. enhancePatternWithExcludes(patterns, excludes, patternPrefix) {
  240. excludes.forEach((exclude) => {
  241. if (exclude.startsWith("!")) {
  242. patterns.push(`${patternPrefix}${exclude.slice(1)}`);
  243. } else {
  244. patterns.push(`!${patternPrefix}${exclude}`);
  245. }
  246. });
  247. }
  248. }
  249. module.exports = AbstractBuilder;