@ui5/builder 3.0.8 → 3.1.0

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,26 @@
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.1.0...HEAD).
6
+
7
+ <a name="v3.1.0"></a>
8
+ ## [v3.1.0] - 2023-10-11
9
+ ### Bug Fixes
10
+ - **bundle/Builder:** Add missing 'names' attribute to generated source maps [`57e0e50`](https://github.com/SAP/ui5-builder/commit/57e0e5047638a9a704a696b8af7780fbbceefbc4)
11
+
12
+ ### Features
13
+ - Validate apiVersion property in Library.init ([#943](https://github.com/SAP/ui5-builder/issues/943)) [`52bf258`](https://github.com/SAP/ui5-builder/commit/52bf25842a59c3fa1ddbe71b482b232e18b55288)
14
+ - **Minifier:** Support input source maps ([#780](https://github.com/SAP/ui5-builder/issues/780)) [`67ecb27`](https://github.com/SAP/ui5-builder/commit/67ecb27e44a2d84e6b2203f31049220dcbcd41f0)
15
+
16
+ ### Reverts
17
+ - [INTERNAL] Azure Pipelines: Switch back to Node 20.5.x
18
+
19
+
20
+ <a name="v3.0.9"></a>
21
+ ## [v3.0.9] - 2023-07-26
6
22
 
7
23
  <a name="v3.0.8"></a>
8
- ## [v3.0.8] - 2023-07-18
24
+ ## [v3.0.8] - 2023-07-19
9
25
  ### Bug Fixes
10
26
  - 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
27
  - **generateJsdoc:** Also process resources created by preceeding tasks [`04447bd`](https://github.com/SAP/ui5-builder/commit/04447bdec28b8bf18c24bd53e3fe8be9bdeed6c2)
@@ -816,6 +832,8 @@ to load the custom bundle file instead.
816
832
 
817
833
  ### Features
818
834
  - Add ability to configure component preloads and custom bundles [`2241e5f`](https://github.com/SAP/ui5-builder/commit/2241e5ff98fd95f1f80cc74959655ae7a9c660e7)
835
+ [v3.1.0]: https://github.com/SAP/ui5-builder/compare/v3.0.9...v3.1.0
836
+ [v3.0.9]: https://github.com/SAP/ui5-builder/compare/v3.0.8...v3.0.9
819
837
  [v3.0.8]: https://github.com/SAP/ui5-builder/compare/v3.0.7...v3.0.8
820
838
  [v3.0.7]: https://github.com/SAP/ui5-builder/compare/v3.0.6...v3.0.7
821
839
  [v3.0.6]: https://github.com/SAP/ui5-builder/compare/v3.0.5...v3.0.6
@@ -61,6 +61,9 @@ async function analyze(resource) {
61
61
  libInfo.elements = getStringArray(value, true);
62
62
  } else if ( ["designtime", "dependencies", "extensions", "name", "version"].includes(key) ) {
63
63
  // do nothing, for all other supported properties
64
+ } else if ( key === "apiVersion" &&
65
+ (value.type === Syntax.Literal && typeof value.value === "number") ) {
66
+ // just a validation
64
67
  } else {
65
68
  log.error(
66
69
  "Unexpected property '" + key +
@@ -18,7 +18,7 @@ import BundleWriter from "./BundleWriter.js";
18
18
  import {getLogger} from "@ui5/logger";
19
19
  const log = getLogger("lbt:bundle:Builder");
20
20
 
21
- const sourceMappingUrlPattern = /\/\/# sourceMappingURL=(.+)\s*$/;
21
+ const sourceMappingUrlPattern = /\/\/# sourceMappingURL=(\S+)\s*$/;
22
22
  const httpPattern = /^https?:\/\//i;
23
23
  const xmlHtmlPrePattern = /<(?:\w+:)?pre\b/;
24
24
 
@@ -332,11 +332,18 @@ class BundleBuilder {
332
332
  throw new Error("No source map provided");
333
333
  }
334
334
 
335
+ // Reminder on the structure of line-segments in the map:
336
+ // [generatedCodeColumn, sourceIndex, sourceCodeLine, sourceCodeColumn, nameIndex]
335
337
  if (map.mappings.startsWith(";")) {
336
338
  // If first line is not already mapped (typical for comments or parentheses), add a mapping to
337
339
  // make sure that dev-tools (especially Chrome's) don't choose the end of the preceding module
338
340
  // when the user tries to set a breakpoint from the bundle file
339
341
  map.mappings = "AAAA" + map.mappings;
342
+ } else if (this.outW.columnOffset === 0 && !map.mappings.startsWith("A")) {
343
+ // If first column of the first line is not already mapped, add a mapping for the same reason as above.
344
+ // This is typical for transpiled code, where there is a bunch of generated code at the beginning that
345
+ // can't be mapped to the original source
346
+ map.mappings = "AAAA," + map.mappings;
340
347
  }
341
348
 
342
349
  map.sourceRoot = path.posix.relative(
@@ -592,9 +599,7 @@ class BundleBuilder {
592
599
  `Attempting to find a source map resource based on the module's path: ${sourceMapFileCandidate}`);
593
600
  try {
594
601
  const sourceMapResource = await this.pool.findResource(sourceMapFileCandidate);
595
- if (sourceMapResource) {
596
- moduleSourceMap = (await sourceMapResource.buffer()).toString();
597
- }
602
+ moduleSourceMap = (await sourceMapResource.buffer()).toString();
598
603
  } catch (e) {
599
604
  // No input source map
600
605
  log.silly(`Could not find a source map for module ${moduleName}: ${e.message}`);
@@ -772,6 +777,7 @@ async function transform(changes, moduleContent, moduleSourceMap) {
772
777
  function createTransientSourceMap({moduleName, moduleContent, includeContent = false}) {
773
778
  const sourceMap = {
774
779
  version: 3,
780
+ names: [],
775
781
  sources: [moduleName],
776
782
  // TODO: check whether moduleContent.match() with \n is better w.r.t performance/memory usage
777
783
  mappings: encodeMappings(moduleContent.split("\n").map((line, i) => {
@@ -1,5 +1,6 @@
1
1
  import {fileURLToPath} from "node:url";
2
2
  import posixPath from "node:path/posix";
3
+ import {promisify} from "node:util";
3
4
  import os from "node:os";
4
5
  import workerpool from "workerpool";
5
6
  import Resource from "@ui5/fs/Resource";
@@ -13,6 +14,9 @@ const MAX_WORKERS = 4;
13
14
  const osCpus = os.cpus().length || 1;
14
15
  const maxWorkers = Math.max(Math.min(osCpus - 1, MAX_WORKERS), MIN_WORKERS);
15
16
 
17
+ const sourceMappingUrlPattern = /\/\/# sourceMappingURL=(\S+)\s*$/;
18
+ const httpPattern = /^https?:\/\//i;
19
+
16
20
  // Shared workerpool across all executions until the taskUtil cleanup is triggered
17
21
  let pool;
18
22
 
@@ -38,6 +42,63 @@ async function minifyInWorker(options, taskUtil) {
38
42
  return getPool(taskUtil).exec("execMinification", [options]);
39
43
  }
40
44
 
45
+ async function extractAndRemoveSourceMappingUrl(resource) {
46
+ const resourceContent = await resource.getString();
47
+ const resourcePath = resource.getPath();
48
+ const sourceMappingUrlMatch = resourceContent.match(sourceMappingUrlPattern);
49
+ if (sourceMappingUrlMatch) {
50
+ const sourceMappingUrl = sourceMappingUrlMatch[1];
51
+ if (log.isLevelEnabled("silly")) {
52
+ log.silly(`Found source map reference in content of resource ${resourcePath}: ${sourceMappingUrl}`);
53
+ }
54
+
55
+ // Strip sourceMappingURL from the resource to be minified
56
+ // It is not required anymore and will be replaced for in the minified resource
57
+ // and its debug variant anyways
58
+ resource.setString(resourceContent.replace(sourceMappingUrlPattern, ""));
59
+ return sourceMappingUrl;
60
+ }
61
+ return null;
62
+ }
63
+
64
+ async function getSourceMapFromUrl({sourceMappingUrl, resourcePath, readFile}) {
65
+ // =======================================================================
66
+ // This code is almost identical to code located in lbt/bundle/Builder.js
67
+ // Please try to update both places when making changes
68
+ // =======================================================================
69
+ if (sourceMappingUrl.startsWith("data:")) {
70
+ // Data-URI indicates an inline source map
71
+ const expectedTypeAndEncoding = "data:application/json;charset=utf-8;base64,";
72
+ if (sourceMappingUrl.startsWith(expectedTypeAndEncoding)) {
73
+ const base64Content = sourceMappingUrl.slice(expectedTypeAndEncoding.length);
74
+ // Create a resource with a path suggesting it's the source map for the resource
75
+ // (which it is but inlined)
76
+ return Buffer.from(base64Content, "base64").toString();
77
+ } else {
78
+ log.warn(
79
+ `Source map reference in resource ${resourcePath} is a data URI but has an unexpected` +
80
+ `encoding: ${sourceMappingUrl}. Expected it to start with ` +
81
+ `"data:application/json;charset=utf-8;base64,"`);
82
+ }
83
+ } else if (httpPattern.test(sourceMappingUrl)) {
84
+ log.warn(`Source map reference in resource ${resourcePath} is an absolute URL. ` +
85
+ `Currently, only relative URLs are supported.`);
86
+ } else if (posixPath.isAbsolute(sourceMappingUrl)) {
87
+ log.warn(`Source map reference in resource ${resourcePath} is an absolute path. ` +
88
+ `Currently, only relative paths are supported.`);
89
+ } else {
90
+ const sourceMapPath = posixPath.join(posixPath.dirname(resourcePath), sourceMappingUrl);
91
+
92
+ try {
93
+ const sourceMapContent = await readFile(sourceMapPath);
94
+ return sourceMapContent.toString();
95
+ } catch (e) {
96
+ // No input source map
97
+ log.warn(`Unable to read source map for resource ${resourcePath}: ${e.message}`);
98
+ }
99
+ }
100
+ }
101
+
41
102
  /**
42
103
  * @public
43
104
  * @module @ui5/builder/processors/minifier
@@ -62,9 +123,16 @@ async function minifyInWorker(options, taskUtil) {
62
123
  *
63
124
  * @param {object} parameters Parameters
64
125
  * @param {@ui5/fs/Resource[]} parameters.resources List of resources to be processed
126
+ * @param {fs|module:@ui5/fs/fsInterface} parameters.fs Node fs or custom
127
+ * [fs interface]{@link module:@ui5/fs/fsInterface}. Required when setting "readSourceMappingUrl" to true
65
128
  * @param {@ui5/builder/tasks/TaskUtil|object} [parameters.taskUtil] TaskUtil instance.
66
129
  * Required when using the <code>useWorkers</code> option
67
130
  * @param {object} [parameters.options] Options
131
+ * @param {boolean} [parameters.options.readSourceMappingUrl=false]
132
+ * Whether to make use of any existing source maps referenced in the resources to be minified. Use this option to
133
+ * preserve references to the original source files, such as TypeScript files, in the generated source map.<br>
134
+ * If a resource has been modified by a previous task, any existing source map will be ignored regardless of this
135
+ * setting. This is to ensure that no inconsistent source maps are used. Check the verbose log for details.
68
136
  * @param {boolean} [parameters.options.addSourceMappingUrl=true]
69
137
  * Whether to add a sourceMappingURL reference to the end of the minified resource
70
138
  * @param {boolean} [parameters.options.useWorkers=false]
@@ -72,8 +140,14 @@ async function minifyInWorker(options, taskUtil) {
72
140
  * @returns {Promise<module:@ui5/builder/processors/minifier~MinifierResult[]>}
73
141
  * Promise resolving with object of resource, dbgResource and sourceMap
74
142
  */
75
- export default async function({resources, taskUtil, options: {addSourceMappingUrl = true, useWorkers = false} = {}}) {
143
+ export default async function({
144
+ resources, fs, taskUtil, options: {readSourceMappingUrl = false, addSourceMappingUrl = true, useWorkers = false
145
+ } = {}}) {
76
146
  let minify;
147
+ if (readSourceMappingUrl && !fs) {
148
+ throw new Error(`Option 'readSourceMappingUrl' requires parameter 'fs' to be provided`);
149
+ }
150
+
77
151
  if (useWorkers) {
78
152
  if (!taskUtil) {
79
153
  // TaskUtil is required for worker support
@@ -86,12 +160,11 @@ export default async function({resources, taskUtil, options: {addSourceMappingUr
86
160
  }
87
161
 
88
162
  return Promise.all(resources.map(async (resource) => {
89
- const dbgPath = resource.getPath().replace(debugFileRegex, "-dbg$1");
90
- const dbgResource = await resource.clone();
91
- dbgResource.setPath(dbgPath);
163
+ const resourcePath = resource.getPath();
164
+ const dbgPath = resourcePath.replace(debugFileRegex, "-dbg$1");
165
+ const dbgFilename = posixPath.basename(dbgPath);
92
166
 
93
167
  const filename = posixPath.basename(resource.getPath());
94
- const code = await resource.getString();
95
168
 
96
169
  const sourceMapOptions = {
97
170
  filename
@@ -99,7 +172,81 @@ export default async function({resources, taskUtil, options: {addSourceMappingUr
99
172
  if (addSourceMappingUrl) {
100
173
  sourceMapOptions.url = filename + ".map";
101
174
  }
102
- const dbgFilename = posixPath.basename(dbgPath);
175
+
176
+ // Remember contentModified flag before making changes to the resource via setString
177
+ const resourceContentModified = resource.getSourceMetadata()?.contentModified;
178
+
179
+ // In any case: Extract *and remove* source map reference from resource before cloning it
180
+ const sourceMappingUrl = await extractAndRemoveSourceMappingUrl(resource);
181
+
182
+ const code = await resource.getString();
183
+ // Create debug variant based off the original resource before minification
184
+ const dbgResource = await resource.clone();
185
+ dbgResource.setPath(dbgPath);
186
+
187
+ let dbgSourceMapResource;
188
+ if (sourceMappingUrl) {
189
+ if (resourceContentModified) {
190
+ log.verbose(
191
+ `Source map found in resource will be ignored because the resource has been ` +
192
+ `modified in a previous task: ${resourcePath}`);
193
+ } else if (readSourceMappingUrl) {
194
+ // Try to find a source map reference in the to-be-minified resource
195
+ // If we find one, provide it to terser as an input source map and keep using it for the
196
+ // debug variant of the resource
197
+ const sourceMapContent = await getSourceMapFromUrl({
198
+ sourceMappingUrl,
199
+ resourcePath,
200
+ readFile: promisify(fs.readFile)
201
+ });
202
+
203
+ if (sourceMapContent) {
204
+ const sourceMapJson = JSON.parse(sourceMapContent);
205
+
206
+ if (sourceMapJson.sections) {
207
+ // TODO 4.0
208
+ // Module "@jridgewell/trace-mapping" (used by Terser) can't handle index map sections lacking
209
+ // a "names" array. Since this is a common occurrence for UI5 Tooling bundles, we search for
210
+ // such cases here and fix them until https://github.com/jridgewell/trace-mapping/pull/29 is
211
+ // resolved and Terser upgraded the dependency
212
+
213
+ // Create a dedicated clone before modifying the source map as to not alter the debug source map
214
+ const clonedSourceMapJson = JSON.parse(sourceMapContent);
215
+ clonedSourceMapJson.sections.forEach(({map}) => {
216
+ if (!map.names) {
217
+ // Add missing names array
218
+ map.names = [];
219
+ }
220
+ });
221
+ // Use modified source map as input source map
222
+ sourceMapOptions.content = JSON.stringify(clonedSourceMapJson);
223
+ } else {
224
+ // Provide source map to terser as "input source map"
225
+ sourceMapOptions.content = sourceMapContent;
226
+ }
227
+
228
+ // Use the original source map for the debug variant of the resource
229
+ // First update the file reference within the source map
230
+ sourceMapJson.file = dbgFilename;
231
+
232
+ // Then create a new resource
233
+ dbgSourceMapResource = new Resource({
234
+ string: JSON.stringify(sourceMapJson),
235
+ path: dbgPath + ".map"
236
+ });
237
+ // And reference the resource in the debug resource
238
+ dbgResource.setString(code + `//# sourceMappingURL=${dbgFilename}.map\n`);
239
+ }
240
+ } else {
241
+ // If the original resource content was unmodified and the input source map was not parsed,
242
+ // re-add the original source map reference to the debug variant
243
+ if (!sourceMappingUrl.startsWith("data:") && !sourceMappingUrl.endsWith(filename + ".map")) {
244
+ // Do not re-add inline source maps as well as references to the source map of
245
+ // the minified resource
246
+ dbgResource.setString(code + `//# sourceMappingURL=${sourceMappingUrl}\n`);
247
+ }
248
+ }
249
+ }
103
250
 
104
251
  const result = await minify({
105
252
  filename,
@@ -112,6 +259,9 @@ export default async function({resources, taskUtil, options: {addSourceMappingUr
112
259
  path: resource.getPath() + ".map",
113
260
  string: result.map
114
261
  });
115
- return {resource, dbgResource, sourceMapResource};
262
+ return {resource, dbgResource, sourceMapResource, dbgSourceMapResource};
116
263
  }));
117
264
  }
265
+
266
+ export const __localFunctions__ = (process.env.NODE_ENV === "test") ?
267
+ {getSourceMapFromUrl} : undefined;
@@ -62,7 +62,9 @@ export default async function execMinification({
62
62
  // Note: err.filename contains the debug-name
63
63
  throw new Error(
64
64
  `Minification failed with error: ${err.message} in file ${filename} ` +
65
- `(line ${err.line}, col ${err.col}, pos ${err.pos})`);
65
+ `(line ${err.line}, col ${err.col}, pos ${err.pos})`, {
66
+ cause: err
67
+ });
66
68
  }
67
69
  }
68
70
 
@@ -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);
@@ -18,8 +18,8 @@ export default function({resources, taskUtil}) {
18
18
  const moduleNameMapping = Object.create(null);
19
19
  for (let i = resources.length - 1; i >= 0; i--) {
20
20
  const resource = resources[i];
21
- if (taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.IsDebugVariant)) {
22
- const resourcePath = resource.getPath();
21
+ const resourcePath = resource.getPath();
22
+ if (resourcePath.endsWith(".js") && taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.IsDebugVariant)) {
23
23
  const nonDbgPath = getNonDebugName(resourcePath);
24
24
  if (!nonDbgPath) {
25
25
  throw new Error(`Failed to resolve non-debug name for ${resourcePath}`);
@@ -1,4 +1,5 @@
1
1
  import minifier from "../processors/minifier.js";
2
+ import fsInterface from "@ui5/fs/fsInterface";
2
3
 
3
4
  /**
4
5
  * @public
@@ -19,20 +20,29 @@ import minifier from "../processors/minifier.js";
19
20
  * @param {string} parameters.options.pattern Pattern to locate the files to be processed
20
21
  * @param {boolean} [parameters.options.omitSourceMapResources=false] Whether source map resources shall
21
22
  * be tagged as "OmitFromBuildResult" and no sourceMappingURL shall be added to the minified resource
23
+ * @param {boolean} [parameters.options.useInputSourceMaps=true] Whether to make use of any existing source
24
+ * maps referenced in the resources to be minified. Use this option to preserve reference to the original
25
+ * source files, such as TypeScript files, in the generated source map.
22
26
  * @returns {Promise<undefined>} Promise resolving with <code>undefined</code> once data has been written
23
27
  */
24
- export default async function({workspace, taskUtil, options: {pattern, omitSourceMapResources = false}}) {
28
+ export default async function({
29
+ workspace, taskUtil, options: {pattern, omitSourceMapResources = false, useInputSourceMaps = true
30
+ }}) {
25
31
  const resources = await workspace.byGlob(pattern);
26
32
  const processedResources = await minifier({
27
33
  resources,
34
+ fs: fsInterface(workspace),
28
35
  taskUtil,
29
36
  options: {
30
37
  addSourceMappingUrl: !omitSourceMapResources,
38
+ readSourceMappingUrl: !!useInputSourceMaps,
31
39
  useWorkers: !!taskUtil,
32
40
  }
33
41
  });
34
42
 
35
- return Promise.all(processedResources.map(async ({resource, dbgResource, sourceMapResource}) => {
43
+ return Promise.all(processedResources.map(async ({
44
+ resource, dbgResource, sourceMapResource, dbgSourceMapResource
45
+ }) => {
36
46
  if (taskUtil) {
37
47
  taskUtil.setTag(resource, taskUtil.STANDARD_TAGS.HasDebugVariant);
38
48
  taskUtil.setTag(dbgResource, taskUtil.STANDARD_TAGS.IsDebugVariant);
@@ -40,11 +50,18 @@ export default async function({workspace, taskUtil, options: {pattern, omitSourc
40
50
  if (omitSourceMapResources) {
41
51
  taskUtil.setTag(sourceMapResource, taskUtil.STANDARD_TAGS.OmitFromBuildResult);
42
52
  }
53
+ if (dbgSourceMapResource) {
54
+ taskUtil.setTag(dbgSourceMapResource, taskUtil.STANDARD_TAGS.IsDebugVariant);
55
+ if (omitSourceMapResources) {
56
+ taskUtil.setTag(dbgSourceMapResource, taskUtil.STANDARD_TAGS.OmitFromBuildResult);
57
+ }
58
+ }
43
59
  }
44
60
  return Promise.all([
45
61
  workspace.write(resource),
46
62
  workspace.write(dbgResource),
47
- workspace.write(sourceMapResource)
63
+ workspace.write(sourceMapResource),
64
+ dbgSourceMapResource && workspace.write(dbgSourceMapResource)
48
65
  ]);
49
66
  }));
50
67
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ui5/builder",
3
- "version": "3.0.8",
3
+ "version": "3.1.0",
4
4
  "description": "UI5 Tooling - Builder",
5
5
  "author": {
6
6
  "name": "SAP SE",
@@ -119,7 +119,7 @@
119
119
  },
120
120
  "dependencies": {
121
121
  "@jridgewell/sourcemap-codec": "^1.4.15",
122
- "@ui5/fs": "^3.0.4",
122
+ "@ui5/fs": "^3.0.5",
123
123
  "@ui5/logger": "^3.0.0",
124
124
  "cheerio": "1.0.0-rc.12",
125
125
  "escape-unicode": "^0.2.0",
@@ -129,31 +129,33 @@
129
129
  "jsdoc": "^4.0.2",
130
130
  "less-openui5": "^0.11.6",
131
131
  "pretty-data": "^0.40.0",
132
- "rimraf": "^5.0.1",
132
+ "rimraf": "^5.0.5",
133
133
  "semver": "^7.5.4",
134
- "terser": "^5.19.0",
135
- "workerpool": "^6.4.0",
136
- "xml2js": "^0.6.0"
134
+ "terser": "^5.21.0",
135
+ "workerpool": "^6.5.0",
136
+ "xml2js": "^0.6.2"
137
137
  },
138
138
  "devDependencies": {
139
139
  "@istanbuljs/esm-loader-hook": "^0.2.0",
140
- "@ui5/project": "^3.4.2",
140
+ "@jridgewell/trace-mapping": "^0.3.19",
141
+ "@ui5/project": "^3.7.1",
141
142
  "ava": "^5.3.1",
142
- "chai": "^4.3.7",
143
+ "chai": "^4.3.10",
143
144
  "chai-fs": "^2.0.0",
144
145
  "chokidar-cli": "^3.0.0",
145
146
  "cross-env": "^7.0.3",
146
- "depcheck": "^1.4.3",
147
- "docdash": "^2.0.1",
148
- "eslint": "^8.45.0",
147
+ "depcheck": "^1.4.6",
148
+ "docdash": "^2.0.2",
149
+ "eslint": "^8.51.0",
149
150
  "eslint-config-google": "^0.14.0",
150
151
  "eslint-plugin-ava": "^14.0.0",
151
- "eslint-plugin-jsdoc": "^46.4.4",
152
- "esmock": "^2.3.1",
152
+ "eslint-plugin-jsdoc": "^46.8.2",
153
+ "esmock": "^2.5.2",
154
+ "line-column": "^1.0.2",
153
155
  "nyc": "^15.1.0",
154
156
  "open-cli": "^7.2.0",
155
157
  "recursive-readdir": "^2.2.3",
156
- "sinon": "^15.2.0",
158
+ "sinon": "^16.1.0",
157
159
  "tap-xunit": "^2.4.1"
158
160
  }
159
161
  }