Skip to content

Commit 29ad613

Browse files
committed
WIP
1 parent 99570a9 commit 29ad613

22 files changed

+1517
-71
lines changed

index.js

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

0 commit comments

Comments
 (0)