builder/lib/tasks/buildThemes.js

  1. import path from "node:path";
  2. import fsInterface from "@ui5/fs/fsInterface";
  3. import ReaderCollectionPrioritized from "@ui5/fs/ReaderCollectionPrioritized";
  4. import {getLogger} from "@ui5/logger";
  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. import {setTimeout as setTimeoutPromise} from "node:timers/promises";
  11. let pool;
  12. function getPool(taskUtil) {
  13. if (!pool) {
  14. const MIN_WORKERS = 2;
  15. const MAX_WORKERS = 4;
  16. const osCpus = os.cpus().length || 1;
  17. const maxWorkers = Math.max(Math.min(osCpus - 1, MAX_WORKERS), MIN_WORKERS);
  18. log.verbose(`Creating workerpool with up to ${maxWorkers} workers (available CPU cores: ${osCpus})`);
  19. const workerPath = fileURLToPath(new URL("../processors/themeBuilderWorker.js", import.meta.url));
  20. pool = workerpool.pool(workerPath, {
  21. workerType: "thread",
  22. maxWorkers
  23. });
  24. taskUtil.registerCleanupTask((force) => {
  25. const attemptPoolTermination = async () => {
  26. log.verbose(`Attempt to terminate the workerpool...`);
  27. if (!pool) {
  28. return;
  29. }
  30. // There are many stats that could be used, but these ones seem the most
  31. // convenient. When all the (available) workers are idle, then it's safe to terminate.
  32. let {idleWorkers, totalWorkers} = pool.stats();
  33. while (idleWorkers !== totalWorkers && !force) {
  34. await setTimeoutPromise(100); // Wait a bit workers to finish and try again
  35. if (!pool) { // pool might have been terminated in the meantime
  36. return;
  37. }
  38. ({idleWorkers, totalWorkers} = pool.stats());
  39. }
  40. const poolToBeTerminated = pool;
  41. pool = null;
  42. return poolToBeTerminated.terminate(force);
  43. };
  44. return attemptPoolTermination();
  45. });
  46. }
  47. return pool;
  48. }
  49. async function buildThemeInWorker(taskUtil, options, transferList) {
  50. const toTransfer = transferList ? {transfer: transferList} : undefined;
  51. return getPool(taskUtil).exec("execThemeBuild", [options], toTransfer);
  52. }
  53. /**
  54. * @public
  55. * @module @ui5/builder/tasks/buildThemes
  56. */
  57. /**
  58. * Task to build a library theme.
  59. *
  60. * @public
  61. * @function default
  62. * @static
  63. *
  64. * @param {object} parameters Parameters
  65. * @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files
  66. * @param {@ui5/fs/AbstractReader} parameters.dependencies Reader or Collection to read dependency files
  67. * @param {@ui5/builder/tasks/TaskUtil|object} [parameters.taskUtil] TaskUtil instance.
  68. * Required to run buildThemes in parallel execution mode.
  69. * @param {object} parameters.options Options
  70. * @param {string} parameters.options.projectName Project name
  71. * @param {string} parameters.options.inputPattern Search pattern for *.less files to be built
  72. * @param {string} [parameters.options.librariesPattern] Search pattern for .library files
  73. * @param {string} [parameters.options.themesPattern] Search pattern for sap.ui.core theme folders
  74. * @param {boolean} [parameters.options.compress=true]
  75. * @param {boolean} [parameters.options.cssVariables=false]
  76. * @returns {Promise<undefined>} Promise resolving with <code>undefined</code> once data has been written
  77. */
  78. export default async function({
  79. workspace, dependencies, taskUtil,
  80. options: {
  81. projectName, inputPattern, librariesPattern, themesPattern, compress,
  82. cssVariables
  83. }
  84. }) {
  85. const combo = new ReaderCollectionPrioritized({
  86. name: `theme - prioritize workspace over dependencies: ${projectName}`,
  87. readers: [workspace, dependencies]
  88. });
  89. compress = compress === undefined ? true : compress;
  90. const pThemeResources = workspace.byGlob(inputPattern);
  91. let pAvailableLibraries;
  92. let pAvailableThemes;
  93. if (librariesPattern) {
  94. // If a librariesPattern is given
  95. // we will use it to reduce the set of libraries a theme will be built for
  96. pAvailableLibraries = combo.byGlob(librariesPattern);
  97. }
  98. if (themesPattern) {
  99. // If a themesPattern is given
  100. // we will use it to reduce the set of themes that will be built
  101. pAvailableThemes = combo.byGlob(themesPattern, {nodir: false});
  102. }
  103. /* Don't try to build themes for libraries that are not available
  104. (maybe replace this with something more aware of which dependencies are optional and therefore
  105. legitimately missing and which not (fault case))
  106. */
  107. let availableLibraries;
  108. if (pAvailableLibraries) {
  109. availableLibraries = [];
  110. (await pAvailableLibraries).forEach((resource) => {
  111. const library = path.dirname(resource.getPath());
  112. if (!availableLibraries.includes(library)) {
  113. availableLibraries.push(library);
  114. }
  115. });
  116. }
  117. let availableThemes;
  118. if (pAvailableThemes) {
  119. availableThemes = (await pAvailableThemes)
  120. .filter((resource) => resource.getStatInfo().isDirectory())
  121. .map((resource) => {
  122. return path.basename(resource.getPath());
  123. });
  124. }
  125. let themeResources = await pThemeResources;
  126. const isAvailable = function(resource) {
  127. let libraryAvailable = false;
  128. let themeAvailable = false;
  129. const resourcePath = resource.getPath();
  130. const themeName = path.basename(path.dirname(resourcePath));
  131. if (!availableLibraries || availableLibraries.length === 0) {
  132. libraryAvailable = true; // If no libraries are found, build themes for all libraries
  133. } else {
  134. for (let i = availableLibraries.length - 1; i >= 0; i--) {
  135. if (resourcePath.startsWith(availableLibraries[i])) {
  136. libraryAvailable = true;
  137. }
  138. }
  139. }
  140. if (!availableThemes || availableThemes.length === 0) {
  141. themeAvailable = true; // If no themes are found, build all themes
  142. } else {
  143. themeAvailable = availableThemes.includes(themeName);
  144. }
  145. if (log.isLevelEnabled("verbose")) {
  146. if (!libraryAvailable) {
  147. log.silly(`Skipping ${resourcePath}: Library is not available`);
  148. }
  149. if (!themeAvailable) {
  150. log.verbose(`Skipping ${resourcePath}: sap.ui.core theme '${themeName}' is not available. ` +
  151. "If you experience missing themes, check whether you have added the corresponding theme " +
  152. "library to your projects dependencies and make sure that your custom themes contain " +
  153. "resources for the sap.ui.core namespace.");
  154. }
  155. }
  156. // Only build if library and theme are available
  157. return libraryAvailable && themeAvailable;
  158. };
  159. if (availableLibraries || availableThemes) {
  160. if (log.isLevelEnabled("verbose")) {
  161. log.verbose("Filtering themes to be built:");
  162. if (availableLibraries) {
  163. log.verbose(`Available libraries: ${availableLibraries.join(", ")}`);
  164. }
  165. if (availableThemes) {
  166. log.verbose(`Available sap.ui.core themes: ${availableThemes.join(", ")}`);
  167. }
  168. }
  169. themeResources = themeResources.filter(isAvailable);
  170. }
  171. let processedResources;
  172. const useWorkers = !!taskUtil;
  173. if (useWorkers) {
  174. const threadMessageHandler = new FsMainThreadInterface(fsInterface(combo));
  175. processedResources = await Promise.all(themeResources.map(async (themeRes) => {
  176. const {port1, port2} = new MessageChannel();
  177. threadMessageHandler.startCommunication(port1);
  178. const result = await buildThemeInWorker(taskUtil, {
  179. fsInterfacePort: port2,
  180. themeResources: await serializeResources([themeRes]),
  181. options: {
  182. compress,
  183. cssVariables: !!cssVariables,
  184. },
  185. }, [port2]);
  186. threadMessageHandler.endCommunication(port1);
  187. return result;
  188. }))
  189. .then((resources) => Array.prototype.concat.apply([], resources))
  190. .then(deserializeResources);
  191. threadMessageHandler.cleanup();
  192. } else {
  193. // Do not use workerpool
  194. const themeBuilder = (await import("../processors/themeBuilder.js")).default;
  195. processedResources = await themeBuilder({
  196. resources: themeResources,
  197. fs: fsInterface(combo),
  198. options: {
  199. compress,
  200. cssVariables: !!cssVariables,
  201. }
  202. });
  203. }
  204. await Promise.all(processedResources.map((resource) => {
  205. return workspace.write(resource);
  206. }));
  207. }