Skip to content

Commit a87578e

Browse files
committed
[POC] Introduce Task Workers
Implementing SAP/ui5-tooling#897 Based on #955
1 parent d6d58b7 commit a87578e

File tree

3 files changed

+82
-76
lines changed

3 files changed

+82
-76
lines changed

lib/processors/minifier.js

Lines changed: 55 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,26 @@ import {fileURLToPath} from "node:url";
22
import posixPath from "node:path/posix";
33
import {promisify} from "node:util";
44
import os from "node:os";
5-
import workerpool from "workerpool";
6-
import Resource from "@ui5/fs/Resource";
7-
import {getLogger} from "@ui5/logger";
8-
const log = getLogger("builder:processors:minifier");
95
import {setTimeout as setTimeoutPromise} from "node:timers/promises";
6+
import {minify} from "terser";
7+
8+
/**
9+
* @private
10+
* @module @ui5/builder/tasks/minifyWorker
11+
*/
12+
13+
/**
14+
* Preserve comments which contain:
15+
* <ul>
16+
* <li>copyright notice</li>
17+
* <li>license terms</li>
18+
* <li>"@ui5-bundle"</li>
19+
* <li>"@ui5-bundle-raw-include"</li>
20+
* </ul>
21+
*
22+
* @type {RegExp}
23+
*/
24+
const copyrightCommentsAndBundleCommentPattern = /copyright|\(c\)(?:[0-9]+|\s+[0-9A-Za-z])|released under|license|\u00a9|^@ui5-bundle-raw-include |^@ui5-bundle /i;
1025

1126
const debugFileRegex = /((?:\.view|\.fragment|\.controller|\.designtime|\.support)?\.js)$/;
1227

@@ -21,40 +36,6 @@ const httpPattern = /^https?:\/\//i;
2136
// Shared workerpool across all executions until the taskUtil cleanup is triggered
2237
let pool;
2338

24-
function getPool(taskUtil) {
25-
if (!pool) {
26-
log.verbose(`Creating workerpool with up to ${maxWorkers} workers (available CPU cores: ${osCpus})`);
27-
const workerPath = fileURLToPath(new URL("./minifierWorker.js", import.meta.url));
28-
pool = workerpool.pool(workerPath, {
29-
workerType: "auto",
30-
maxWorkers
31-
});
32-
taskUtil.registerCleanupTask((force) => {
33-
const attemptPoolTermination = async () => {
34-
log.verbose(`Attempt to terminate the workerpool...`);
35-
36-
if (!pool) {
37-
return;
38-
}
39-
40-
// There are many stats that could be used, but these ones seem the most
41-
// convenient. When all the (available) workers are idle, then it's safe to terminate.
42-
let {idleWorkers, totalWorkers} = pool.stats();
43-
while (idleWorkers !== totalWorkers && !force) {
44-
await setTimeoutPromise(100); // Wait a bit workers to finish and try again
45-
({idleWorkers, totalWorkers} = pool.stats());
46-
}
47-
48-
const poolToBeTerminated = pool;
49-
pool = null;
50-
return poolToBeTerminated.terminate(force);
51-
};
52-
53-
return attemptPoolTermination();
54-
});
55-
}
56-
return pool;
57-
}
5839

5940
async function minifyInWorker(options, taskUtil) {
6041
return getPool(taskUtil).exec("execMinification", [options]);
@@ -159,24 +140,12 @@ async function getSourceMapFromUrl({sourceMappingUrl, resourcePath, readFile}) {
159140
* Promise resolving with object of resource, dbgResource and sourceMap
160141
*/
161142
export default async function({
162-
resources, fs, taskUtil, options: {readSourceMappingUrl = false, addSourceMappingUrl = true, useWorkers = false
163-
} = {}}) {
164-
let minify;
143+
resources, fs, options: {readSourceMappingUrl = false, addSourceMappingUrl = true, useWorkers = false
144+
} = {}, log, resourceFactory}) {
165145
if (readSourceMappingUrl && !fs) {
166146
throw new Error(`Option 'readSourceMappingUrl' requires parameter 'fs' to be provided`);
167147
}
168148

169-
if (useWorkers) {
170-
if (!taskUtil) {
171-
// TaskUtil is required for worker support
172-
throw new Error(`Minifier: Option 'useWorkers' requires a taskUtil instance to be provided`);
173-
}
174-
minify = minifyInWorker;
175-
} else {
176-
// Do not use workerpool
177-
minify = (await import("./minifierWorker.js")).default;
178-
}
179-
180149
return Promise.all(resources.map(async (resource) => {
181150
const resourcePath = resource.getPath();
182151
const dbgPath = resourcePath.replace(debugFileRegex, "-dbg$1");
@@ -248,7 +217,7 @@ export default async function({
248217
sourceMapJson.file = dbgFilename;
249218

250219
// Then create a new resource
251-
dbgSourceMapResource = new Resource({
220+
dbgSourceMapResource = resourceFactory.createResource({
252221
string: JSON.stringify(sourceMapJson),
253222
path: dbgPath + ".map"
254223
});
@@ -265,19 +234,39 @@ export default async function({
265234
}
266235
}
267236
}
268-
269-
const result = await minify({
270-
filename,
271-
dbgFilename,
272-
code,
273-
sourceMapOptions
274-
}, taskUtil);
275-
resource.setString(result.code);
276-
const sourceMapResource = new Resource({
277-
path: resource.getPath() + ".map",
278-
string: result.map
279-
});
280-
return {resource, dbgResource, sourceMapResource, dbgSourceMapResource};
237+
try {
238+
const result = await minify({
239+
// Use debug-name since this will be referenced in the source map "sources"
240+
[dbgFilename]: code
241+
}, {
242+
output: {
243+
comments: copyrightCommentsAndBundleCommentPattern,
244+
wrap_func_args: false
245+
},
246+
compress: false,
247+
mangle: {
248+
reserved: [
249+
"jQuery",
250+
"jquery",
251+
"sap",
252+
]
253+
},
254+
sourceMap: sourceMapOptions
255+
});
256+
resource.setString(result.code);
257+
const sourceMapResource = resourceFactory.createResource({
258+
path: resource.getPath() + ".map",
259+
string: result.map
260+
});
261+
return {resource, dbgResource, sourceMapResource, dbgSourceMapResource};
262+
} catch (err) {
263+
// Note: err.filename contains the debug-name
264+
throw new Error(
265+
`Minification failed with error: ${err.message} in file ${filename} ` +
266+
`(line ${err.line}, col ${err.col}, pos ${err.pos})`, {
267+
cause: err
268+
});
269+
}
281270
}));
282271
}
283272

lib/tasks/minify.js

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
import minifier from "../processors/minifier.js";
2-
import fsInterface from "@ui5/fs/fsInterface";
3-
41
/**
52
* @public
63
* @module @ui5/builder/tasks/minify
@@ -16,6 +13,7 @@ import fsInterface from "@ui5/fs/fsInterface";
1613
* @param {object} parameters Parameters
1714
* @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files
1815
* @param {@ui5/project/build/helpers/TaskUtil|object} [parameters.taskUtil] TaskUtil
16+
* @param {object} parameters.processors
1917
* @param {object} parameters.options Options
2018
* @param {string} parameters.options.pattern Pattern to locate the files to be processed
2119
* @param {boolean} [parameters.options.omitSourceMapResources=false] Whether source map resources shall
@@ -26,13 +24,12 @@ import fsInterface from "@ui5/fs/fsInterface";
2624
* @returns {Promise<undefined>} Promise resolving with <code>undefined</code> once data has been written
2725
*/
2826
export default async function({
29-
workspace, taskUtil, options: {pattern, omitSourceMapResources = false, useInputSourceMaps = true
30-
}}) {
27+
workspace, taskUtil, processors, options: {pattern, omitSourceMapResources = false, useInputSourceMaps = true}
28+
}) {
3129
const resources = await workspace.byGlob(pattern);
32-
const processedResources = await minifier({
30+
const processedResources = await processors.execute("minifier", {
3331
resources,
34-
fs: fsInterface(workspace),
35-
taskUtil,
32+
reader: workspace,
3633
options: {
3734
addSourceMappingUrl: !omitSourceMapResources,
3835
readSourceMappingUrl: !!useInputSourceMaps,

lib/tasks/taskRepository.js

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {createRequire} from "node:module";
2+
import {fileURLToPath} from "node:url";
23

34
/**
45
* Repository providing access to all UI5 Builder tasks and various metadata required by the build process.
@@ -21,7 +22,14 @@ const taskInfos = {
2122
executeJsdocSdkTransformation: {path: "./jsdoc/executeJsdocSdkTransformation.js"},
2223
generateApiIndex: {path: "./jsdoc/generateApiIndex.js"},
2324
generateJsdoc: {path: "./jsdoc/generateJsdoc.js"},
24-
minify: {path: "./minify.js"},
25+
minify: {
26+
path: "./minify.js",
27+
processors: {
28+
minifier: {
29+
path: "../processors/minifier.js"
30+
}
31+
}
32+
},
2533
buildThemes: {path: "./buildThemes.js"},
2634
transformBootstrapHtml: {path: "./transformBootstrapHtml.js"},
2735
generateLibraryManifest: {path: "./generateLibraryManifest.js"},
@@ -67,8 +75,20 @@ export async function getTask(taskName) {
6775
}
6876
try {
6977
const {default: task} = await import(taskInfo.path);
78+
let processors = null;
79+
if (taskInfo.processors) {
80+
processors = Object.create(null);
81+
for (const processorName in taskInfo.processors) {
82+
if (Object.hasOwn(taskInfo.processors, processorName)) {
83+
processors[processorName] = {
84+
path: fileURLToPath(new URL(taskInfo.processors[processorName].path, import.meta.url))
85+
};
86+
}
87+
}
88+
}
7089
return {
71-
task
90+
task,
91+
processors
7292
};
7393
} catch (err) {
7494
throw new Error(`taskRepository: Failed to require task module for ${taskName}: ${err.message}`);

0 commit comments

Comments
 (0)