@ui5/builder 3.0.7 → 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.
@@ -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);
@@ -154,12 +154,7 @@ const utils = {
154
154
  virBasePath: "/resources/"
155
155
  });
156
156
 
157
- let allResources;
158
- if (workspace.byGlobSource) { // API only available on duplex collections
159
- allResources = await workspace.byGlobSource(pattern);
160
- } else {
161
- allResources = await workspace.byGlob(pattern);
162
- }
157
+ const allResources = await workspace.byGlob(pattern);
163
158
 
164
159
  // write all resources to the tmp folder
165
160
  await Promise.all(allResources.map((resource) => fsTarget.write(resource)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ui5/builder",
3
- "version": "3.0.7",
3
+ "version": "3.0.9",
4
4
  "description": "UI5 Tooling - Builder",
5
5
  "author": {
6
6
  "name": "SAP SE",
@@ -38,7 +38,7 @@
38
38
  "unit": "rimraf test/tmp && ava",
39
39
  "unit-verbose": "rimraf test/tmp && cross-env UI5_LOG_LVL=verbose ava --verbose --serial",
40
40
  "unit-watch": "rimraf test/tmp && ava --watch",
41
- "unit-xunit": "rimraf test/tmp && ava --no-worker-threads --node-arguments=\"--experimental-loader=@istanbuljs/esm-loader-hook\" --tap --timeout=1m | tap-xunit --dontUseCommentsAsTestNames=true > test-results.xml",
41
+ "unit-xunit": "rimraf test/tmp && ava --node-arguments=\"--experimental-loader=@istanbuljs/esm-loader-hook\" --tap --timeout=1m | tap-xunit --dontUseCommentsAsTestNames=true > test-results.xml",
42
42
  "unit-inspect": "cross-env UI5_LOG_LVL=verbose ava debug --break",
43
43
  "coverage": "rimraf test/tmp && nyc ava --node-arguments=\"--experimental-loader=@istanbuljs/esm-loader-hook\"",
44
44
  "coverage-xunit": "nyc --reporter=text --reporter=text-summary --reporter=cobertura npm run unit-xunit",
@@ -69,7 +69,8 @@
69
69
  "nodeArguments": [
70
70
  "--loader=esmock",
71
71
  "--no-warnings"
72
- ]
72
+ ],
73
+ "workerThreads": false
73
74
  },
74
75
  "nyc": {
75
76
  "reporter": [
@@ -123,20 +124,20 @@
123
124
  "cheerio": "1.0.0-rc.12",
124
125
  "escape-unicode": "^0.2.0",
125
126
  "escope": "^4.0.0",
126
- "espree": "^9.6.0",
127
+ "espree": "^9.6.1",
127
128
  "graceful-fs": "^4.2.11",
128
129
  "jsdoc": "^4.0.2",
129
130
  "less-openui5": "^0.11.6",
130
131
  "pretty-data": "^0.40.0",
131
132
  "rimraf": "^5.0.1",
132
133
  "semver": "^7.5.4",
133
- "terser": "^5.18.2",
134
+ "terser": "^5.19.2",
134
135
  "workerpool": "^6.4.0",
135
136
  "xml2js": "^0.6.0"
136
137
  },
137
138
  "devDependencies": {
138
139
  "@istanbuljs/esm-loader-hook": "^0.2.0",
139
- "@ui5/project": "^3.4.1",
140
+ "@ui5/project": "^3.4.2",
140
141
  "ava": "^5.3.1",
141
142
  "chai": "^4.3.7",
142
143
  "chai-fs": "^2.0.0",
@@ -144,11 +145,11 @@
144
145
  "cross-env": "^7.0.3",
145
146
  "depcheck": "^1.4.3",
146
147
  "docdash": "^2.0.1",
147
- "eslint": "^8.44.0",
148
+ "eslint": "^8.45.0",
148
149
  "eslint-config-google": "^0.14.0",
149
150
  "eslint-plugin-ava": "^14.0.0",
150
- "eslint-plugin-jsdoc": "^46.4.3",
151
- "esmock": "^2.3.1",
151
+ "eslint-plugin-jsdoc": "^46.4.4",
152
+ "esmock": "^2.3.2",
152
153
  "nyc": "^15.1.0",
153
154
  "open-cli": "^7.2.0",
154
155
  "recursive-readdir": "^2.2.3",