@ui5/builder 3.0.8 → 3.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,10 +2,13 @@
2
2
  All notable changes to this project will be documented in this file.
3
3
  This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
4
4
 
5
- A list of unreleased changes can be found [here](https://github.com/SAP/ui5-builder/compare/v3.0.8...HEAD).
5
+ A list of unreleased changes can be found [here](https://github.com/SAP/ui5-builder/compare/v3.0.9...HEAD).
6
+
7
+ <a name="v3.0.9"></a>
8
+ ## [v3.0.9] - 2023-07-25
6
9
 
7
10
  <a name="v3.0.8"></a>
8
- ## [v3.0.8] - 2023-07-18
11
+ ## [v3.0.8] - 2023-07-19
9
12
  ### Bug Fixes
10
13
  - Revert "[INTERNAL] Remove implicit dependencies concept ([#913](https://github.com/SAP/ui5-builder/issues/913))" [`1043714`](https://github.com/SAP/ui5-builder/commit/1043714e3b952a8280f1ff7909f79db3b750eb0c)
11
14
  - **generateJsdoc:** Also process resources created by preceeding tasks [`04447bd`](https://github.com/SAP/ui5-builder/commit/04447bdec28b8bf18c24bd53e3fe8be9bdeed6c2)
@@ -816,6 +819,7 @@ to load the custom bundle file instead.
816
819
 
817
820
  ### Features
818
821
  - Add ability to configure component preloads and custom bundles [`2241e5f`](https://github.com/SAP/ui5-builder/commit/2241e5ff98fd95f1f80cc74959655ae7a9c660e7)
822
+ [v3.0.9]: https://github.com/SAP/ui5-builder/compare/v3.0.8...v3.0.9
819
823
  [v3.0.8]: https://github.com/SAP/ui5-builder/compare/v3.0.7...v3.0.8
820
824
  [v3.0.7]: https://github.com/SAP/ui5-builder/compare/v3.0.6...v3.0.7
821
825
  [v3.0.6]: https://github.com/SAP/ui5-builder/compare/v3.0.5...v3.0.6
@@ -0,0 +1,278 @@
1
+ import workerpool from "workerpool";
2
+ import themeBuilder from "./themeBuilder.js";
3
+ import {createResource} from "@ui5/fs/resourceFactory";
4
+ import {Buffer} from "node:buffer";
5
+
6
+ /**
7
+ * Task to build library themes.
8
+ *
9
+ * @private
10
+ * @function default
11
+ * @static
12
+ *
13
+ * @param {object} parameters Parameters
14
+ * @param {MessagePort} parameters.fsInterfacePort
15
+ * @param {object[]} parameters.themeResources Input array of Uint8Array transferable objects
16
+ * that are the less sources to build upon. By nature those are @ui5/fs/Resource.
17
+ * @param {object} parameters.options Less compiler options
18
+ * @returns {Promise<object[]>} Resulting array of Uint8Array transferable objects
19
+ */
20
+ export default async function execThemeBuild({
21
+ fsInterfacePort,
22
+ themeResources = [],
23
+ options = {}
24
+ }) {
25
+ const fsThemeResources = deserializeResources(themeResources);
26
+ const fsReader = new FsWorkerThreadInterface(fsInterfacePort);
27
+
28
+ const result = await themeBuilder({
29
+ resources: fsThemeResources,
30
+ fs: fsReader,
31
+ options
32
+ });
33
+
34
+ return serializeResources(result);
35
+ }
36
+
37
+ /**
38
+ * Casts @ui5/fs/Resource-s into an Uint8Array transferable object
39
+ *
40
+ * @param {@ui5/fs/Resource[]} resourceCollection
41
+ * @returns {Promise<object[]>}
42
+ */
43
+ export async function serializeResources(resourceCollection) {
44
+ return Promise.all(
45
+ resourceCollection.map(async (res) => ({
46
+ buffer: await res.getBuffer(),
47
+ path: res.getPath()
48
+ }))
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Casts Uint8Array into @ui5/fs/Resource-s transferable object
54
+ *
55
+ * @param {Promise<object[]>} resources
56
+ * @returns {@ui5/fs/Resource[]}
57
+ */
58
+ export function deserializeResources(resources) {
59
+ return resources.map((res) => {
60
+ // res.buffer is an Uint8Array object and needs to be cast
61
+ // to a Buffer in order to be read correctly.
62
+ return createResource({path: res.path, buffer: Buffer.from(res.buffer)});
63
+ });
64
+ }
65
+
66
+ /**
67
+ * "@ui5/fs/fsInterface" like class that uses internally
68
+ * "@ui5/fs/fsInterface", implements its methods, and
69
+ * sends the results to a MessagePort.
70
+ *
71
+ * Used in the main thread in a combination with FsWorkerThreadInterface.
72
+ */
73
+ export class FsMainThreadInterface {
74
+ #comPorts = new Set();
75
+ #fsInterfaceReader = null;
76
+ #cache = Object.create(null);
77
+
78
+ /**
79
+ * Constructor
80
+ *
81
+ * @param {@ui5/fs/fsInterface} fsInterfaceReader Reader for the Resources
82
+ */
83
+ constructor(fsInterfaceReader) {
84
+ if (!fsInterfaceReader) {
85
+ throw new Error("fsInterfaceReader is mandatory argument");
86
+ }
87
+
88
+ this.#fsInterfaceReader = fsInterfaceReader;
89
+ }
90
+
91
+ /**
92
+ * Adds MessagePort and starts listening for requests on it.
93
+ *
94
+ * @param {MessagePort} comPort port1 from a {code}MessageChannel{/code}
95
+ */
96
+ startCommunication(comPort) {
97
+ if (!comPort) {
98
+ throw new Error("Communication channel is mandatory argument");
99
+ }
100
+
101
+ this.#comPorts.add(comPort);
102
+ comPort.on("message", (e) => this.#onMessage(e, comPort));
103
+ comPort.on("close", () => comPort.close());
104
+ }
105
+
106
+ /**
107
+ * Ends MessagePort communication.
108
+ *
109
+ * @param {MessagePort} comPort port1 to remove from handling.
110
+ */
111
+ endCommunication(comPort) {
112
+ comPort.close();
113
+ this.#comPorts.delete(comPort);
114
+ }
115
+
116
+ /**
117
+ * Destroys the FsMainThreadInterface
118
+ */
119
+ cleanup() {
120
+ this.#comPorts.forEach((comPort) => comPort.close());
121
+ this.#cache = null;
122
+ this.#fsInterfaceReader = null;
123
+ }
124
+
125
+ /**
126
+ * Handles messages from the MessagePort
127
+ *
128
+ * @param {object} e data to construct the request
129
+ * @param {string} e.action Action to perform. Corresponds to the names of
130
+ * the public methods of "@ui5/fs/fsInterface"
131
+ * @param {string} e.fsPath Path of the Resource
132
+ * @param {object} e.options Options for "readFile" action
133
+ * @param {MessagePort} comPort The communication channel
134
+ */
135
+ #onMessage(e, comPort) {
136
+ switch (e.action) {
137
+ case "readFile":
138
+ this.#doRequest(comPort, {action: "readFile", fsPath: e.fsPath, options: e.options});
139
+ break;
140
+ case "stat":
141
+ this.#doRequest(comPort, {action: "stat", fsPath: e.fsPath});
142
+ break;
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Requests a Resource from the "@ui5/fs/fsInterface" and sends it to the worker threads
148
+ * via postMessage.
149
+ *
150
+ * @param {MessagePort} comPort The communication channel
151
+ * @param {object} parameters
152
+ * @param {string} parameters.action Action to perform. Corresponds to the names of
153
+ * the public methods of "@ui5/fs/fsInterface" and triggers this method of the
154
+ * "@ui5/fs/fsInterface" instance.
155
+ * @param {string} parameters.fsPath Path of the Resource
156
+ * @param {object} parameters.options Options for "readFile" action
157
+ */
158
+ async #doRequest(comPort, {action, fsPath, options}) {
159
+ const cacheKey = `${fsPath}-${action}`;
160
+ if (!this.#cache[cacheKey]) {
161
+ this.#cache[cacheKey] = new Promise((res) => {
162
+ if (action === "readFile") {
163
+ this.#fsInterfaceReader.readFile(fsPath, options, (error, result) => res({error, result}));
164
+ } else if (action === "stat") {
165
+ this.#fsInterfaceReader.stat(fsPath, (error, result) =>
166
+ // The Stat object has some special methods that sometimes cannot be serialized
167
+ // properly in the postMessage. In this scenario, we do not need those methods,
168
+ // but just to check whether stats has resolved to something.
169
+ res(JSON.parse(JSON.stringify({error, result})))
170
+ );
171
+ } else {
172
+ res({error: new Error(`Action "${action}" is not available.`), result: null});
173
+ }
174
+ });
175
+ }
176
+
177
+ const fromCache = await this.#cache[cacheKey];
178
+ comPort.postMessage({action, fsPath, ...fromCache});
179
+ }
180
+ }
181
+
182
+ /**
183
+ * "@ui5/fs/fsInterface" like class that uses internally
184
+ * "@ui5/fs/fsInterface", implements its methods, and
185
+ * requests resources via MessagePort.
186
+ *
187
+ * Used in the main thread in a combination with FsMainThreadInterface.
188
+ */
189
+ export class FsWorkerThreadInterface {
190
+ #comPort = null;
191
+ #callbacks = [];
192
+ #cache = Object.create(null);
193
+
194
+ /**
195
+ * Constructor
196
+ *
197
+ * @param {MessagePort} comPort Communication port
198
+ */
199
+ constructor(comPort) {
200
+ if (!comPort) {
201
+ throw new Error("Communication port is mandatory argument");
202
+ }
203
+
204
+ this.#comPort = comPort;
205
+ comPort.on("message", this.#onMessage.bind(this));
206
+ comPort.on("close", this.#onClose.bind(this));
207
+ }
208
+
209
+ /**
210
+ * Handles messages from MessagePort
211
+ *
212
+ * @param {object} e
213
+ * @param {string} e.action Action to perform. Corresponds to the names of
214
+ * the public methods of "@ui5/fs/fsInterface"
215
+ * @param {string} e.fsPath Path of the Resource
216
+ * @param {*} e.result Response from the "action".
217
+ * @param {object} e.error Error from the "action".
218
+ */
219
+ #onMessage(e) {
220
+ const cbObject = this.#callbacks.find((cb) => cb.action === e.action && cb.fsPath === e.fsPath);
221
+
222
+ if (cbObject) {
223
+ this.#cache[`${e.fsPath}-${e.action}`] = {error: e.error, result: e.result};
224
+ this.#callbacks.splice(this.#callbacks.indexOf(cbObject), 1);
225
+ cbObject.callback(e.error, e.result);
226
+ } else {
227
+ throw new Error("No callback found for this message! Possible hang for the thread!", e);
228
+ }
229
+ }
230
+
231
+ /**
232
+ * End communication
233
+ */
234
+ #onClose() {
235
+ this.#comPort.close();
236
+ this.#cache = null;
237
+ }
238
+
239
+ /**
240
+ * Makes a request via the MessagePort
241
+ *
242
+ * @param {object} parameters
243
+ * @param {string} parameters.action Action to perform. Corresponds to the names of
244
+ * the public methods.
245
+ * @param {string} parameters.fsPath Path of the Resource
246
+ * @param {object} parameters.options Options for "readFile" action
247
+ * @param {Function} callback Callback to call when the "action" is executed and ready.
248
+ */
249
+ #doRequest({action, fsPath, options}, callback) {
250
+ const cacheKey = `${fsPath}-${action}`;
251
+
252
+ if (this.#cache[cacheKey]) {
253
+ const {result, error} = this.#cache[cacheKey];
254
+ callback(error, result);
255
+ } else {
256
+ this.#callbacks.push({action, fsPath, callback});
257
+ this.#comPort.postMessage({action, fsPath, options});
258
+ }
259
+ }
260
+
261
+ readFile(fsPath, options, callback) {
262
+ this.#doRequest({action: "readFile", fsPath, options}, callback);
263
+ }
264
+
265
+ stat(fsPath, callback) {
266
+ this.#doRequest({action: "stat", fsPath}, callback);
267
+ }
268
+ }
269
+
270
+ // Test execution via ava is never done on the main thread
271
+ /* istanbul ignore else */
272
+ if (!workerpool.isMainThread) {
273
+ // Script got loaded through workerpool
274
+ // => Create a worker and register public functions
275
+ workerpool.worker({
276
+ execThemeBuild
277
+ });
278
+ }
@@ -1,9 +1,44 @@
1
1
  import path from "node:path";
2
- import themeBuilder from "../processors/themeBuilder.js";
3
2
  import fsInterface from "@ui5/fs/fsInterface";
4
3
  import ReaderCollectionPrioritized from "@ui5/fs/ReaderCollectionPrioritized";
5
4
  import {getLogger} from "@ui5/logger";
6
5
  const log = getLogger("builder:tasks:buildThemes");
6
+ import {fileURLToPath} from "node:url";
7
+ import os from "node:os";
8
+ import workerpool from "workerpool";
9
+ import {deserializeResources, serializeResources, FsMainThreadInterface} from "../processors/themeBuilderWorker.js";
10
+
11
+ let pool;
12
+
13
+ function getPool(taskUtil) {
14
+ if (!pool) {
15
+ const MIN_WORKERS = 2;
16
+ const MAX_WORKERS = 4;
17
+ const osCpus = os.cpus().length || 1;
18
+ const maxWorkers = Math.max(Math.min(osCpus - 1, MAX_WORKERS), MIN_WORKERS);
19
+
20
+ log.verbose(`Creating workerpool with up to ${maxWorkers} workers (available CPU cores: ${osCpus})`);
21
+ const workerPath = fileURLToPath(new URL("../processors/themeBuilderWorker.js", import.meta.url));
22
+ pool = workerpool.pool(workerPath, {
23
+ workerType: "thread",
24
+ maxWorkers
25
+ });
26
+ taskUtil.registerCleanupTask(() => {
27
+ log.verbose(`Terminating workerpool`);
28
+ const poolToBeTerminated = pool;
29
+ pool = null;
30
+ poolToBeTerminated.terminate();
31
+ });
32
+ }
33
+ return pool;
34
+ }
35
+
36
+ async function buildThemeInWorker(taskUtil, options, transferList) {
37
+ const toTransfer = transferList ? {transfer: transferList} : undefined;
38
+
39
+ return getPool(taskUtil).exec("execThemeBuild", [options], toTransfer);
40
+ }
41
+
7
42
 
8
43
  /**
9
44
  * @public
@@ -19,6 +54,8 @@ const log = getLogger("builder:tasks:buildThemes");
19
54
  * @param {object} parameters Parameters
20
55
  * @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files
21
56
  * @param {@ui5/fs/AbstractReader} parameters.dependencies Reader or Collection to read dependency files
57
+ * @param {@ui5/builder/tasks/TaskUtil|object} [parameters.taskUtil] TaskUtil instance.
58
+ * Required to run buildThemes in parallel execution mode.
22
59
  * @param {object} parameters.options Options
23
60
  * @param {string} parameters.options.projectName Project name
24
61
  * @param {string} parameters.options.inputPattern Search pattern for *.less files to be built
@@ -29,9 +66,10 @@ const log = getLogger("builder:tasks:buildThemes");
29
66
  * @returns {Promise<undefined>} Promise resolving with <code>undefined</code> once data has been written
30
67
  */
31
68
  export default async function({
32
- workspace, dependencies,
69
+ workspace, dependencies, taskUtil,
33
70
  options: {
34
- projectName, inputPattern, librariesPattern, themesPattern, compress, cssVariables
71
+ projectName, inputPattern, librariesPattern, themesPattern, compress,
72
+ cssVariables
35
73
  }
36
74
  }) {
37
75
  const combo = new ReaderCollectionPrioritized({
@@ -41,7 +79,7 @@ export default async function({
41
79
 
42
80
  compress = compress === undefined ? true : compress;
43
81
 
44
- const pAllResources = workspace.byGlob(inputPattern);
82
+ const pThemeResources = workspace.byGlob(inputPattern);
45
83
  let pAvailableLibraries;
46
84
  let pAvailableThemes;
47
85
  if (librariesPattern) {
@@ -78,7 +116,7 @@ export default async function({
78
116
  });
79
117
  }
80
118
 
81
- let allResources = await pAllResources;
119
+ let themeResources = await pThemeResources;
82
120
 
83
121
  const isAvailable = function(resource) {
84
122
  let libraryAvailable = false;
@@ -128,17 +166,48 @@ export default async function({
128
166
  log.verbose(`Available sap.ui.core themes: ${availableThemes.join(", ")}`);
129
167
  }
130
168
  }
131
- allResources = allResources.filter(isAvailable);
169
+ themeResources = themeResources.filter(isAvailable);
132
170
  }
133
171
 
134
- const processedResources = await themeBuilder({
135
- resources: allResources,
136
- fs: fsInterface(combo),
137
- options: {
138
- compress,
139
- cssVariables: !!cssVariables
140
- }
141
- });
172
+ let processedResources;
173
+ const useWorkers = !!taskUtil;
174
+ if (useWorkers) {
175
+ const threadMessageHandler = new FsMainThreadInterface(fsInterface(combo));
176
+
177
+ processedResources = await Promise.all(themeResources.map(async (themeRes) => {
178
+ const {port1, port2} = new MessageChannel();
179
+ threadMessageHandler.startCommunication(port1);
180
+
181
+ const result = await buildThemeInWorker(taskUtil, {
182
+ fsInterfacePort: port2,
183
+ themeResources: await serializeResources([themeRes]),
184
+ options: {
185
+ compress,
186
+ cssVariables: !!cssVariables,
187
+ },
188
+ }, [port2]);
189
+
190
+ threadMessageHandler.endCommunication(port1);
191
+
192
+ return result;
193
+ }))
194
+ .then((resources) => Array.prototype.concat.apply([], resources))
195
+ .then(deserializeResources);
196
+
197
+ threadMessageHandler.cleanup();
198
+ } else {
199
+ // Do not use workerpool
200
+ const themeBuilder = (await import("../processors/themeBuilder.js")).default;
201
+
202
+ processedResources = await themeBuilder({
203
+ resources: themeResources,
204
+ fs: fsInterface(combo),
205
+ options: {
206
+ compress,
207
+ cssVariables: !!cssVariables,
208
+ }
209
+ });
210
+ }
142
211
 
143
212
  await Promise.all(processedResources.map((resource) => {
144
213
  return workspace.write(resource);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ui5/builder",
3
- "version": "3.0.8",
3
+ "version": "3.0.9",
4
4
  "description": "UI5 Tooling - Builder",
5
5
  "author": {
6
6
  "name": "SAP SE",
@@ -131,7 +131,7 @@
131
131
  "pretty-data": "^0.40.0",
132
132
  "rimraf": "^5.0.1",
133
133
  "semver": "^7.5.4",
134
- "terser": "^5.19.0",
134
+ "terser": "^5.19.2",
135
135
  "workerpool": "^6.4.0",
136
136
  "xml2js": "^0.6.0"
137
137
  },
@@ -149,7 +149,7 @@
149
149
  "eslint-config-google": "^0.14.0",
150
150
  "eslint-plugin-ava": "^14.0.0",
151
151
  "eslint-plugin-jsdoc": "^46.4.4",
152
- "esmock": "^2.3.1",
152
+ "esmock": "^2.3.2",
153
153
  "nyc": "^15.1.0",
154
154
  "open-cli": "^7.2.0",
155
155
  "recursive-readdir": "^2.2.3",