builder/lib/builder/builder.js

  1. const {promisify} = require("util");
  2. const rimraf = promisify(require("rimraf"));
  3. const log = require("@ui5/logger").getGroupLogger("builder:builder");
  4. const resourceFactory = require("@ui5/fs").resourceFactory;
  5. const MemAdapter = require("@ui5/fs").adapters.Memory;
  6. const typeRepository = require("../types/typeRepository");
  7. const taskRepository = require("../tasks/taskRepository");
  8. const BuildContext = require("./BuildContext");
  9. // Set of tasks for development
  10. const devTasks = [
  11. "replaceCopyright",
  12. "replaceVersion",
  13. "replaceBuildtime",
  14. "buildThemes"
  15. ];
  16. /**
  17. * Calculates the elapsed build time and returns a prettified output
  18. *
  19. * @private
  20. * @param {Array} startTime Array provided by <code>process.hrtime()</code>
  21. * @returns {string} Difference between now and the provided time array as formatted string
  22. */
  23. function getElapsedTime(startTime) {
  24. const prettyHrtime = require("pretty-hrtime");
  25. const timeDiff = process.hrtime(startTime);
  26. return prettyHrtime(timeDiff);
  27. }
  28. /**
  29. * Creates the list of tasks to be executed by the build process
  30. *
  31. * Sets specific tasks to be disabled by default, these tasks need to be included explicitly.
  32. * Based on the selected build mode (dev|selfContained|preload), different tasks are enabled.
  33. * Tasks can be enabled or disabled. The wildcard <code>*</code> is also supported and affects all tasks.
  34. *
  35. * @private
  36. * @param {object} parameters
  37. * @param {boolean} parameters.dev Sets development mode, which only runs essential tasks
  38. * @param {boolean} parameters.selfContained
  39. * True if a the build should be self-contained or false for prelead build bundles
  40. * @param {boolean} parameters.jsdoc True if a JSDoc build should be executed
  41. * @param {Array} parameters.includedTasks Task list to be included from build
  42. * @param {Array} parameters.excludedTasks Task list to be excluded from build
  43. * @returns {Array} Return a task list for the builder
  44. */
  45. function composeTaskList({dev, selfContained, jsdoc, includedTasks, excludedTasks}) {
  46. const definedTasks = taskRepository.getAllTaskNames();
  47. let selectedTasks = definedTasks.reduce((list, key) => {
  48. list[key] = true;
  49. return list;
  50. }, {});
  51. // Exclude non default tasks
  52. selectedTasks.generateManifestBundle = false;
  53. selectedTasks.generateStandaloneAppBundle = false;
  54. selectedTasks.transformBootstrapHtml = false;
  55. selectedTasks.generateJsdoc = false;
  56. selectedTasks.executeJsdocSdkTransformation = false;
  57. selectedTasks.generateCachebusterInfo = false;
  58. selectedTasks.generateApiIndex = false;
  59. selectedTasks.generateThemeDesignerResources = false;
  60. // Disable generateResourcesJson due to performance.
  61. // When executed it analyzes each module's AST and therefore
  62. // takes up much time (~10% more)
  63. selectedTasks.generateResourcesJson = false;
  64. if (selfContained) {
  65. // No preloads, bundle only
  66. selectedTasks.generateComponentPreload = false;
  67. selectedTasks.generateStandaloneAppBundle = true;
  68. selectedTasks.transformBootstrapHtml = true;
  69. selectedTasks.generateLibraryPreload = false;
  70. }
  71. // TODO 3.0: exclude generateVersionInfo if not --all is used
  72. if (jsdoc) {
  73. // Include JSDoc tasks
  74. selectedTasks.generateJsdoc = true;
  75. selectedTasks.executeJsdocSdkTransformation = true;
  76. selectedTasks.generateApiIndex = true;
  77. // Include theme build as required for SDK
  78. selectedTasks.buildThemes = true;
  79. // Exclude all tasks not relevant to JSDoc generation
  80. selectedTasks.replaceCopyright = false;
  81. selectedTasks.replaceVersion = false;
  82. selectedTasks.replaceBuildtime = false;
  83. selectedTasks.generateComponentPreload = false;
  84. selectedTasks.generateLibraryPreload = false;
  85. selectedTasks.generateLibraryManifest = false;
  86. selectedTasks.createDebugFiles = false;
  87. selectedTasks.uglify = false;
  88. selectedTasks.generateFlexChangesBundle = false;
  89. selectedTasks.generateManifestBundle = false;
  90. }
  91. // Only run essential tasks in development mode, it is not desired to run time consuming tasks during development.
  92. if (dev) {
  93. // Overwrite all other tasks with noop promise
  94. Object.keys(selectedTasks).forEach((key) => {
  95. if (devTasks.indexOf(key) === -1) {
  96. selectedTasks[key] = false;
  97. }
  98. });
  99. }
  100. // Exclude tasks
  101. for (let i = 0; i < excludedTasks.length; i++) {
  102. const taskName = excludedTasks[i];
  103. if (taskName === "*") {
  104. Object.keys(selectedTasks).forEach((sKey) => {
  105. selectedTasks[sKey] = false;
  106. });
  107. break;
  108. }
  109. if (selectedTasks[taskName] === true) {
  110. selectedTasks[taskName] = false;
  111. } else if (typeof selectedTasks[taskName] === "undefined") {
  112. log.warn(`Unable to exclude task '${taskName}': Task is unknown`);
  113. }
  114. }
  115. // Include tasks
  116. for (let i = 0; i < includedTasks.length; i++) {
  117. const taskName = includedTasks[i];
  118. if (taskName === "*") {
  119. Object.keys(selectedTasks).forEach((sKey) => {
  120. selectedTasks[sKey] = true;
  121. });
  122. break;
  123. }
  124. if (selectedTasks[taskName] === false) {
  125. selectedTasks[taskName] = true;
  126. } else if (typeof selectedTasks[taskName] === "undefined") {
  127. log.warn(`Unable to include task '${taskName}': Task is unknown`);
  128. }
  129. }
  130. // Filter only for tasks that will be executed
  131. selectedTasks = Object.keys(selectedTasks).filter((task) => selectedTasks[task]);
  132. return selectedTasks;
  133. }
  134. async function executeCleanupTasks(buildContext) {
  135. log.info("Executing cleanup tasks...");
  136. await buildContext.executeCleanupTasks();
  137. }
  138. function registerCleanupSigHooks(buildContext) {
  139. function createListener(exitCode) {
  140. return function() {
  141. // Asynchronously cleanup resources, then exit
  142. executeCleanupTasks(buildContext).then(() => {
  143. process.exit(exitCode);
  144. });
  145. };
  146. }
  147. const processSignals = {
  148. "SIGHUP": createListener(128 + 1),
  149. "SIGINT": createListener(128 + 2),
  150. "SIGTERM": createListener(128 + 15),
  151. "SIGBREAK": createListener(128 + 21)
  152. };
  153. for (const signal of Object.keys(processSignals)) {
  154. process.on(signal, processSignals[signal]);
  155. }
  156. // == TO BE DISCUSSED: Also cleanup for unhandled rejections and exceptions?
  157. // Add additional events like signals since they are registered on the process
  158. // event emitter in a similar fashion
  159. // processSignals["unhandledRejection"] = createListener(1);
  160. // process.once("unhandledRejection", processSignals["unhandledRejection"]);
  161. // processSignals["uncaughtException"] = function(err, origin) {
  162. // const fs = require("fs");
  163. // fs.writeSync(
  164. // process.stderr.fd,
  165. // `Caught exception: ${err}\n` +
  166. // `Exception origin: ${origin}`
  167. // );
  168. // createListener(1)();
  169. // };
  170. // process.once("uncaughtException", processSignals["uncaughtException"]);
  171. return processSignals;
  172. }
  173. function deregisterCleanupSigHooks(signals) {
  174. for (const signal of Object.keys(signals)) {
  175. process.removeListener(signal, signals[signal]);
  176. }
  177. }
  178. /**
  179. * Builder
  180. *
  181. * @public
  182. * @namespace
  183. * @alias module:@ui5/builder.builder
  184. */
  185. module.exports = {
  186. /**
  187. * Configures the project build and starts it.
  188. *
  189. * @public
  190. * @param {object} parameters Parameters
  191. * @param {object} parameters.tree Project tree as generated by the
  192. * [@ui5/project.normalizer]{@link module:@ui5/project.normalizer}
  193. * @param {string} parameters.destPath Target path
  194. * @param {boolean} [parameters.cleanDest=false] Decides whether project should clean the target path before build
  195. * @param {boolean} [parameters.buildDependencies=false] Decides whether project dependencies are built as well
  196. * @param {Array.<string|RegExp>} [parameters.includedDependencies=[]]
  197. * List of build dependencies to be included if buildDependencies is true
  198. * @param {Array.<string|RegExp>} [parameters.excludedDependencies=[]]
  199. * List of build dependencies to be excluded if buildDependencies is true.
  200. * If the wildcard '*' is provided, only the included dependencies will be built.
  201. * @param {boolean} [parameters.dev=false]
  202. * Decides whether a development build should be activated (skips non-essential and time-intensive tasks)
  203. * @param {boolean} [parameters.selfContained=false] Flag to activate self contained build
  204. * @param {boolean} [parameters.jsdoc=false] Flag to activate JSDoc build
  205. * @param {Array.<string>} [parameters.includedTasks=[]] List of tasks to be included
  206. * @param {Array.<string>} [parameters.excludedTasks=[]] List of tasks to be excluded.
  207. * If the wildcard '*' is provided, only the included tasks will be executed.
  208. * @param {Array.<string>} [parameters.devExcludeProject=[]] List of projects to be excluded from development build
  209. * @returns {Promise} Promise resolving to <code>undefined</code> once build has finished
  210. */
  211. async build({
  212. tree, destPath, cleanDest = false,
  213. buildDependencies = false, includedDependencies = [], excludedDependencies = [],
  214. dev = false, selfContained = false, jsdoc = false,
  215. includedTasks = [], excludedTasks = [], devExcludeProject = []
  216. }) {
  217. const startTime = process.hrtime();
  218. log.info(`Building project ${tree.metadata.name}` + (buildDependencies ? "" : " not") +
  219. " including dependencies..." + (dev ? " [dev mode]" : ""));
  220. log.verbose(`Building to ${destPath}...`);
  221. const selectedTasks = composeTaskList({dev, selfContained, jsdoc, includedTasks, excludedTasks});
  222. const fsTarget = resourceFactory.createAdapter({
  223. fsBasePath: destPath,
  224. virBasePath: "/"
  225. });
  226. const buildContext = new BuildContext({rootProject: tree});
  227. const cleanupSigHooks = registerCleanupSigHooks(buildContext);
  228. const projects = {}; // Unique project index to prevent building the same project multiple times
  229. const projectWriters = {}; // Collection of memory adapters of already built libraries
  230. function projectFilter(project) {
  231. function projectMatchesAny(deps) {
  232. return deps.some((dep) => dep instanceof RegExp ?
  233. dep.test(project.metadata.name) : dep === project.metadata.name);
  234. }
  235. // if everything is included, this overrules exclude lists
  236. if (includedDependencies.includes("*")) return true;
  237. let test = !excludedDependencies.includes("*"); // exclude everything?
  238. if (test && projectMatchesAny(excludedDependencies)) {
  239. test = false;
  240. }
  241. if (!test && projectMatchesAny(includedDependencies)) {
  242. test = true;
  243. }
  244. return test;
  245. }
  246. const projectCountMarker = {};
  247. function projectCount(project, count = 0) {
  248. if (buildDependencies) {
  249. count = project.dependencies.filter(projectFilter).reduce((depCount, depProject) => {
  250. return projectCount(depProject, depCount);
  251. }, count);
  252. }
  253. if (!projectCountMarker[project.metadata.name]) {
  254. count++;
  255. projectCountMarker[project.metadata.name] = true;
  256. }
  257. return count;
  258. }
  259. const buildLogger = log.createTaskLogger("🛠 ", projectCount(tree));
  260. function buildProject(project) {
  261. const projectBasePath = `/resources/${project.metadata.namespace}`;
  262. let depPromise;
  263. let projectTasks = selectedTasks;
  264. // Build dependencies in sequence as it is far easier to detect issues and reduces
  265. // side effects or other issues such as too many open files
  266. if (buildDependencies) {
  267. depPromise = project.dependencies.filter(projectFilter).reduce(function(p, depProject) {
  268. return p.then(() => buildProject(depProject));
  269. }, Promise.resolve());
  270. } else {
  271. depPromise = Promise.resolve();
  272. }
  273. // Build the project after all dependencies have been built
  274. return depPromise.then(() => {
  275. if (projects[project.metadata.name]) {
  276. return Promise.resolve();
  277. } else {
  278. projects[project.metadata.name] = true;
  279. }
  280. buildLogger.startWork(`Building project ${project.metadata.name}`);
  281. const projectType = typeRepository.getType(project.type);
  282. const resourceCollections = resourceFactory.createCollectionsForTree(project, {
  283. virtualReaders: projectWriters,
  284. getVirtualBasePathPrefix: function({project, virBasePath}) {
  285. if (project.type === "application" && project.metadata.namespace) {
  286. return projectBasePath;
  287. }
  288. },
  289. getProjectExcludes: function(project) {
  290. if (project.builder && project.builder.resources) {
  291. return project.builder.resources.excludes;
  292. }
  293. }
  294. });
  295. const writer = new MemAdapter({
  296. virBasePath: "/"
  297. });
  298. // Store project writer as virtual reader for parent projects
  299. // so they can access the build results of this project
  300. projectWriters[project.metadata.name] = writer;
  301. // TODO: Add getter for writer of DuplexColection
  302. const workspace = resourceFactory.createWorkspace({
  303. virBasePath: "/",
  304. writer,
  305. reader: resourceCollections.source,
  306. name: project.metadata.name
  307. });
  308. const projectContext = buildContext.createProjectContext({
  309. project, // TODO 2.0: Add project facade object/instance here
  310. resources: {
  311. workspace,
  312. dependencies: resourceCollections.dependencies
  313. }
  314. });
  315. const TaskUtil = require("../tasks/TaskUtil");
  316. const taskUtil = new TaskUtil({
  317. projectBuildContext: projectContext
  318. });
  319. if (dev && devExcludeProject.indexOf(project.metadata.name) !== -1) {
  320. projectTasks = composeTaskList({dev: false, selfContained, includedTasks, excludedTasks});
  321. }
  322. return projectType.build({
  323. resourceCollections: {
  324. workspace,
  325. dependencies: resourceCollections.dependencies
  326. },
  327. tasks: projectTasks,
  328. project,
  329. parentLogger: log,
  330. taskUtil
  331. }).then(() => {
  332. log.verbose("Finished building project %s. Writing out files...", project.metadata.name);
  333. buildLogger.completeWork(1);
  334. return workspace.byGlob("/**/*").then((resources) => {
  335. const tagCollection = projectContext.getResourceTagCollection();
  336. return Promise.all(resources.map((resource) => {
  337. if (tagCollection.getTag(resource, projectContext.STANDARD_TAGS.OmitFromBuildResult)) {
  338. log.verbose(`Skipping write of resource tagged as "OmitFromBuildResult": ` +
  339. resource.getPath());
  340. return; // Skip target write for this resource
  341. }
  342. if (projectContext.isRootProject() && project.type === "application" &&
  343. project.metadata.namespace) {
  344. // Root-application projects only: Remove namespace prefix if given
  345. const resourcePath = resource.getPath();
  346. if (resourcePath.startsWith(projectBasePath)) {
  347. resource.setPath(resourcePath.replace(projectBasePath, ""));
  348. }
  349. }
  350. return fsTarget.write(resource);
  351. }));
  352. });
  353. });
  354. });
  355. }
  356. try {
  357. if (cleanDest) {
  358. await rimraf(destPath);
  359. }
  360. await buildProject(tree);
  361. log.info(`Build succeeded in ${getElapsedTime(startTime)}`);
  362. } catch (err) {
  363. log.error(`Build failed in ${getElapsedTime(startTime)}`);
  364. throw err;
  365. } finally {
  366. deregisterCleanupSigHooks(cleanupSigHooks);
  367. await executeCleanupTasks(buildContext);
  368. }
  369. }
  370. };
  371. // Export local function for testing only
  372. /* istanbul ignore else */
  373. if (process.env.NODE_ENV === "test") {
  374. module.exports._composeTaskList = composeTaskList;
  375. }