Skip to content

Commit a555c9b

Browse files
committed
[BREAKING] Implement Project Graph, build execution
* Replace the JSON-object based dependency tree handling with a graph representation * Projects are now represented by classes with documented APIs * Projects can be accessed by extensions defining specVersion >=2.7 * Speed up resolution of package.json dependencies * Make "ui5.dependencies" package.json configuration obsolete * Move build execution from ui5-builder to ui5-project * ui5-builder scope reduced top provides task implementations only * Build: Determine automatically whether a project-build requires dependencies to be built and build them * Build: Add new option 'createBuildManifest'. This will create a manifest file in the target directory that allows reuse of the build result of library and theme-library projects in other project builds (RFC0011) This PR will need additional follow-up to add more test cases, cleanup JSDoc and possibly add more features described in the RFCs. This is a nicer version of #394 Implements RFC0009: UI5/cli#501 Implements RFC0011: UI5/cli#612 BREAKING CHANGE: * normalizer and projectTree APIs have been removed. Use generateProjectGraph instead * Going forward only specification versions 2.0 and higher are supported * In case a legacy specification version is detected, an automatic, transparent migration is attempted. * Build: * The "dev" build mode has been removed * The task "generateVersionInfo" is no longer executed for application projects by default. You may enable it again using the includedTasks parameter
1 parent f7afb59 commit a555c9b

File tree

201 files changed

+13270
-7799
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

201 files changed

+13270
-7799
lines changed

index.js

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44
*/
55
module.exports = {
66
/**
7-
* @type {import('./lib/normalizer')}
7+
* @type {import('./lib/builder')}
88
*/
9-
normalizer: "./lib/normalizer",
9+
builder: "./lib/builder",
1010
/**
11-
* @type {import('./lib/projectPreprocessor')}
11+
* @type {import('./lib/generateProjectGraph')}
1212
*/
13-
projectPreprocessor: "./lib/projectPreprocessor",
13+
generateProjectGraph: "./lib/generateProjectGraph",
1414
/**
1515
* @public
1616
* @alias module:@ui5/project.ui5Framework
@@ -42,20 +42,20 @@ module.exports = {
4242
ValidationError: "./lib/validation/ValidationError"
4343
},
4444
/**
45-
* @private
46-
* @alias module:@ui5/project.translators
45+
* @public
46+
* @alias module:@ui5/project.graph
4747
* @namespace
4848
*/
49-
translators: {
49+
graph: {
5050
/**
51-
* @type {import('./lib/translators/npm')}
51+
* @type {typeof import('./lib/graph/ProjectGraph')}
5252
*/
53-
npm: "./lib/translators/npm",
53+
ProjectGraph: "./lib/graph/ProjectGraph",
5454
/**
55-
* @type {import('./lib/translators/static')}
55+
* @type {typeof import('./lib/graph/projectGraphBuilder')}
5656
*/
57-
static: "./lib/translators/static"
58-
}
57+
projectGraphBuilder: "./lib/graph/projectGraphBuilder",
58+
},
5959
};
6060

6161
function exportModules(exportRoot, modulePaths) {
Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
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+
*/
28+
constructor({graph, project, parentLogger, taskUtil}) {
29+
if (new.target === AbstractBuilder) {
30+
throw new TypeError("Class 'AbstractBuilder' is abstract");
31+
}
32+
33+
this.project = project;
34+
this.graph = graph;
35+
this.taskUtil = taskUtil;
36+
37+
this.log = parentLogger.createSubLogger(project.getType() + " " + project.getName(), 0.2);
38+
this.taskLog = this.log.createTaskLogger("🔨");
39+
40+
this.tasks = {};
41+
this.taskExecutionOrder = [];
42+
43+
this.addStandardTasks({
44+
project,
45+
taskUtil,
46+
getTask
47+
});
48+
this.addCustomTasks({
49+
graph,
50+
project,
51+
taskUtil
52+
});
53+
}
54+
55+
/**
56+
* Adds all standard tasks to execute
57+
*
58+
* @abstract
59+
* @protected
60+
* @param {object} parameters
61+
* @param {object} parameters.taskUtil
62+
* @param {object} parameters.project
63+
*/
64+
addStandardTasks({project, taskUtil}) {
65+
throw new Error("Function 'addStandardTasks' is not implemented");
66+
}
67+
68+
/**
69+
* Adds custom tasks to execute
70+
*
71+
* @private
72+
* @param {object} parameters
73+
* @param {object} parameters.graph
74+
* @param {object} parameters.project
75+
* @param {object} parameters.taskUtil
76+
*/
77+
addCustomTasks({graph, project, taskUtil}) {
78+
const projectCustomTasks = project.getCustomTasks();
79+
if (!projectCustomTasks || projectCustomTasks.length === 0) {
80+
return; // No custom tasks defined
81+
}
82+
for (let i = 0; i < projectCustomTasks.length; i++) {
83+
const taskDef = projectCustomTasks[i];
84+
if (!taskDef.name) {
85+
throw new Error(`Missing name for custom task definition of project ${project.getName()} ` +
86+
`at index ${i}`);
87+
}
88+
if (taskDef.beforeTask && taskDef.afterTask) {
89+
throw new Error(`Custom task definition ${taskDef.name} of project ${project.getName()} ` +
90+
`defines both "beforeTask" and "afterTask" parameters. Only one must be defined.`);
91+
}
92+
if (this.taskExecutionOrder.length && !taskDef.beforeTask && !taskDef.afterTask) {
93+
// Iff there are tasks configured, beforeTask or afterTask must be given
94+
throw new Error(`Custom task definition ${taskDef.name} of project ${project.getName()} ` +
95+
`defines neither a "beforeTask" nor an "afterTask" parameter. One must be defined.`);
96+
}
97+
98+
let newTaskName = taskDef.name;
99+
if (this.tasks[newTaskName]) {
100+
// Task is already known
101+
// => add a suffix to allow for multiple configurations of the same task
102+
let suffixCounter = 0;
103+
while (this.tasks[newTaskName]) {
104+
suffixCounter++; // Start at 1
105+
newTaskName = `${taskDef.name}--${suffixCounter}`;
106+
}
107+
}
108+
const task = graph.getExtension(taskDef.name);
109+
// TODO: Create callback for custom tasks to configure "requiresDependencies" and "enabled"
110+
// Input: task "options" and build mode ("standalone", "preload", etc.)
111+
const requiresDependencies = true; // Default to true for old spec versions
112+
const execTask = function({workspace, dependencies}) {
113+
/* Custom Task Interface
114+
Parameters:
115+
{Object} parameters Parameters
116+
{module:@ui5/fs.DuplexCollection} parameters.workspace DuplexCollection to read and write files
117+
{module:@ui5/fs.AbstractReader} parameters.dependencies
118+
Reader or Collection to read dependency files
119+
{Object} parameters.taskUtil Specification Version dependent interface to a
120+
[TaskUtil]{@link module:@ui5/builder.tasks.TaskUtil} instance
121+
{Object} parameters.options Options
122+
{string} parameters.options.projectName Project name
123+
{string} [parameters.options.projectNamespace] Project namespace if available
124+
{string} [parameters.options.configuration] Task configuration if given in ui5.yaml
125+
Returns:
126+
{Promise<undefined>} Promise resolving with undefined once data has been written
127+
*/
128+
const params = {
129+
workspace,
130+
options: {
131+
projectName: project.getName(),
132+
projectNamespace: project.getNamespace(),
133+
configuration: taskDef.configuration
134+
}
135+
};
136+
137+
if (requiresDependencies) {
138+
params.dependencies = dependencies;
139+
}
140+
141+
const taskUtilInterface = taskUtil.getInterface(task.getSpecVersion());
142+
// Interface is undefined if specVersion does not support taskUtil
143+
if (taskUtilInterface) {
144+
params.taskUtil = taskUtilInterface;
145+
}
146+
return task.getTask()(params);
147+
};
148+
149+
this.tasks[newTaskName] = {
150+
task: execTask,
151+
requiresDependencies
152+
};
153+
154+
if (this.taskExecutionOrder.length) {
155+
// There is at least one task configured. Use before- and afterTask to add the custom task
156+
const refTaskName = taskDef.beforeTask || taskDef.afterTask;
157+
let refTaskIdx = this.taskExecutionOrder.indexOf(refTaskName);
158+
if (refTaskIdx === -1) {
159+
throw new Error(`Could not find task ${refTaskName}, referenced by custom task ${newTaskName}, ` +
160+
`to be scheduled for project ${project.getName()}`);
161+
}
162+
if (taskDef.afterTask) {
163+
// Insert after index of referenced task
164+
refTaskIdx++;
165+
}
166+
this.taskExecutionOrder.splice(refTaskIdx, 0, newTaskName);
167+
} else {
168+
// There is no task configured so far. Just add the custom task
169+
this.taskExecutionOrder.push(newTaskName);
170+
}
171+
}
172+
}
173+
174+
/**
175+
* Adds a executable task to the builder
176+
*
177+
* The order this function is being called defines the build order. FIFO.
178+
*
179+
* @param {string} taskName Name of the task which should be in the list availableTasks.
180+
* @param {object} [parameters]
181+
* @param {boolean} [parameters.requiresDependencies]
182+
* @param {object} [parameters.options]
183+
* @param {Function} [taskFunction]
184+
*/
185+
addTask(taskName, {requiresDependencies = false, options = {}} = {}, taskFunction) {
186+
if (this.tasks[taskName]) {
187+
throw new Error(`Failed to add duplicate task ${taskName} for project ${this.project.getName()}`);
188+
}
189+
if (this.taskExecutionOrder.includes(taskName)) {
190+
throw new Error(`Builder: Failed to add task ${taskName} for project ${this.project.getName()}. ` +
191+
`It has already been scheduled for execution.`);
192+
}
193+
194+
const task = ({workspace, dependencies}) => {
195+
options.projectName = this.project.getName();
196+
// TODO: Deprecate "namespace" in favor of "projectNamespace" as already used for custom tasks?
197+
options.projectNamespace = this.project.getNamespace();
198+
199+
const params = {
200+
workspace,
201+
taskUtil: this.taskUtil,
202+
options
203+
};
204+
205+
if (requiresDependencies) {
206+
params.dependencies = dependencies;
207+
}
208+
209+
if (!taskFunction) {
210+
taskFunction = getTask(taskName).task;
211+
}
212+
return taskFunction(params);
213+
};
214+
this.tasks[taskName] = {
215+
task,
216+
requiresDependencies
217+
};
218+
this.taskExecutionOrder.push(taskName);
219+
}
220+
221+
/**
222+
* Takes a list of tasks which should be executed from the available task list of the current builder
223+
*
224+
* @param {object} buildConfig
225+
* @param {boolean} buildConfig.selfContained
226+
* True if a the build should be self-contained or false for prelead build bundles
227+
* @param {boolean} buildConfig.jsdoc True if a JSDoc build should be executed
228+
* @param {Array} buildConfig.includedTasks Task list to be included from build
229+
* @param {Array} buildConfig.excludedTasks Task list to be excluded from build
230+
* @param {object} buildParams
231+
* @param {module:@ui5/fs.DuplexCollection} buildParams.workspace Workspace of the current project
232+
* @param {module:@ui5/fs.ReaderCollection} buildParams.dependencies Dependencies reader collection
233+
* @returns {Promise} Returns promise chain with tasks
234+
*/
235+
async build(buildConfig, buildParams) {
236+
const tasksToRun = composeTaskList(Object.keys(this.tasks), buildConfig);
237+
const allTasks = this.taskExecutionOrder.filter((taskName) => {
238+
// There might be a numeric suffix in case a custom task is configured multiple times.
239+
// The suffix needs to be removed in order to check against the list of tasks to run.
240+
//
241+
// Note: The 'tasksToRun' parameter only allows to specify the custom task name
242+
// (without suffix), so it executes either all or nothing.
243+
// It's currently not possible to just execute some occurrences of a custom task.
244+
// This would require a more robust contract to identify task executions
245+
// (e.g. via an 'id' that can be assigned to a specific execution in the configuration).
246+
const taskWithoutSuffixCounter = taskName.replace(/--\d+$/, "");
247+
return tasksToRun.includes(taskWithoutSuffixCounter);
248+
});
249+
250+
this.taskLog.addWork(allTasks.length);
251+
252+
for (const taskName of allTasks) {
253+
const taskFunction = this.tasks[taskName].task;
254+
255+
if (typeof taskFunction === "function") {
256+
await this.executeTask(taskName, taskFunction, buildParams);
257+
}
258+
}
259+
}
260+
261+
requiresDependencies(buildConfig) {
262+
const tasksToRun = composeTaskList(Object.keys(this.tasks), buildConfig);
263+
const allTasks = this.taskExecutionOrder.filter((taskName) => {
264+
// There might be a numeric suffix in case a custom task is configured multiple times.
265+
// The suffix needs to be removed in order to check against the list of tasks to run.
266+
//
267+
// Note: The 'tasksToRun' parameter only allows to specify the custom task name
268+
// (without suffix), so it executes either all or nothing.
269+
// It's currently not possible to just execute some occurrences of a custom task.
270+
// This would require a more robust contract to identify task executions
271+
// (e.g. via an 'id' that can be assigned to a specific execution in the configuration).
272+
const taskWithoutSuffixCounter = taskName.replace(/--\d+$/, "");
273+
return tasksToRun.includes(taskWithoutSuffixCounter);
274+
});
275+
return allTasks.some((taskName) => {
276+
if (this.tasks[taskName].requiresDependencies) {
277+
this.log.verbose(`Task ${taskName} for project ${this.project.getName()} requires dependencies`);
278+
return true;
279+
}
280+
return false;
281+
});
282+
}
283+
284+
/**
285+
* Adds progress related functionality to task function.
286+
*
287+
* @private
288+
* @param {string} taskName Name of the task
289+
* @param {Function} taskFunction Function which executed the task
290+
* @param {object} taskParams Base parameters for all tasks
291+
* @returns {Promise} Resolves when task has finished
292+
*/
293+
async executeTask(taskName, taskFunction, taskParams) {
294+
this.taskLog.startWork(`Running task ${taskName}...`);
295+
this._taskStart = performance.now();
296+
await taskFunction(taskParams);
297+
this.taskLog.completeWork(1);
298+
if (process.env.UI5_LOG_TASK_PERF) {
299+
this.taskLog.info(`Task succeeded in ${Math.round((performance.now() - this._taskStart))} ms`);
300+
}
301+
}
302+
303+
/**
304+
* Appends the list of 'excludes' to the list of 'patterns'. To harmonize both lists, the 'excludes'
305+
* are negated and the 'patternPrefix' is added to make them absolute.
306+
*
307+
* @private
308+
* @param {string[]} patterns
309+
* List of absolute default patterns.
310+
* @param {string[]} excludes
311+
* List of relative patterns to be excluded. Excludes with a leading "!" are meant to be re-included.
312+
* @param {string} patternPrefix
313+
* Prefix to be added to the excludes to make them absolute. The prefix must have a leading and a
314+
* trailing "/".
315+
*/
316+
enhancePatternWithExcludes(patterns, excludes, patternPrefix) {
317+
excludes.forEach((exclude) => {
318+
if (exclude.startsWith("!")) {
319+
patterns.push(`${patternPrefix}${exclude.slice(1)}`);
320+
} else {
321+
patterns.push(`!${patternPrefix}${exclude}`);
322+
}
323+
});
324+
}
325+
}
326+
327+
module.exports = AbstractBuilder;

0 commit comments

Comments
 (0)