builder/lib/processors/versionInfoGenerator.js

  1. const log = require("@ui5/logger").getLogger("builder:processors:versionInfoGenerator");
  2. const resourceFactory = require("@ui5/fs").resourceFactory;
  3. const posixPath = require("path").posix;
  4. function pad(v) {
  5. return String(v).padStart(2, "0");
  6. }
  7. function getTimestamp() {
  8. const date = new Date();
  9. const year = date.getFullYear();
  10. const month = pad(date.getMonth() + 1);
  11. const day = pad(date.getDate());
  12. const hours = pad(date.getHours());
  13. const minutes = pad(date.getMinutes());
  14. // yyyyMMddHHmm
  15. return year + month + day + hours + minutes;
  16. }
  17. /**
  18. * Manifest libraries as defined in the manifest.json file
  19. *
  20. * @typedef {object<string, {lazy: boolean}>} ManifestLibraries
  21. *
  22. * sample:
  23. * <pre>
  24. * {
  25. * "sap.chart": {
  26. * "lazy": true
  27. * },
  28. * "sap.f": { }
  29. * }
  30. * </pre>
  31. */
  32. /**
  33. * Extracted information from a manifest's <code>sap.app</code> and <code>sap.ui5</code> sections.
  34. *
  35. * @typedef {object} ManifestInfo
  36. *
  37. * @property {string} id The library name, e.g. "lib.x"
  38. * @property {string} embeddedBy the library this component is embedded in, e.g. "lib.x"
  39. * @property {string[]} embeds the embedded component names, e.g. ["lib.x.sub"]
  40. * @property {ManifestLibraries} libs the dependencies, e.g. {"sap.chart":{"lazy": true}, "sap.f":{}}
  41. */
  42. /**
  43. * Processes manifest resource and extracts information.
  44. *
  45. * @param {module:@ui5/fs.Resource} manifestResource
  46. * @returns {Promise<ManifestInfo>}
  47. */
  48. const processManifest = async (manifestResource) => {
  49. const manifestContent = await manifestResource.getString();
  50. const manifestObject = JSON.parse(manifestContent);
  51. const manifestInfo = {};
  52. // sap.ui5/dependencies is used for the "manifestHints/libs"
  53. if (manifestObject["sap.ui5"]) {
  54. const manifestDependencies = manifestObject["sap.ui5"]["dependencies"];
  55. if (manifestDependencies && manifestDependencies.libs) {
  56. const libs = {};
  57. for (const [libKey, libValue] of Object.entries(manifestDependencies.libs)) {
  58. libs[libKey] = {};
  59. if (libValue.lazy) {
  60. libs[libKey].lazy = true;
  61. }
  62. }
  63. manifestInfo.libs = libs;
  64. }
  65. }
  66. // sap.app/embeds, sap.app/embeddedBy and sap.app/id is used for "components"
  67. if (manifestObject["sap.app"]) {
  68. const manifestEmbeds = manifestObject["sap.app"]["embeds"];
  69. manifestInfo.embeds = manifestEmbeds;
  70. const manifestEmbeddedBy = manifestObject["sap.app"]["embeddedBy"];
  71. manifestInfo.embeddedBy = manifestEmbeddedBy;
  72. const id = manifestObject["sap.app"]["id"];
  73. manifestInfo.id = id;
  74. }
  75. return manifestInfo;
  76. };
  77. /**
  78. * Checks if a component (componentPath) is bundled with the library (embeddedBy)
  79. *
  80. * @param {string} embeddedBy e.g. "../"
  81. * @param {string} componentPath e.g. "lib/x/sub"
  82. * @param {string} libraryPathPrefix e.g. "lib/x"
  83. * @returns {boolean} whether or not this component is bundled with the library
  84. */
  85. const isBundledWithLibrary = (embeddedBy, componentPath, libraryPathPrefix) => {
  86. if (typeof embeddedBy === "undefined") {
  87. log.verbose(" component doesn't declare 'sap.app/embeddedBy', don't list it as 'embedded'");
  88. return false;
  89. }
  90. if (typeof embeddedBy !== "string") {
  91. log.error(
  92. " component '%s': property 'sap.app/embeddedBy' is of type '%s' (expected 'string'), " +
  93. "it won't be listed as 'embedded'", componentPath, typeof embeddedBy
  94. );
  95. return false;
  96. }
  97. if ( !embeddedBy.length ) {
  98. log.error(
  99. " component '%s': property 'sap.app/embeddedBy' has an empty string value (which is invalid), " +
  100. "it won't be listed as 'embedded'", componentPath
  101. );
  102. return false;
  103. }
  104. let resolvedEmbeddedBy = posixPath.resolve(componentPath, embeddedBy);
  105. if ( resolvedEmbeddedBy && !resolvedEmbeddedBy.endsWith("/") ) {
  106. resolvedEmbeddedBy = resolvedEmbeddedBy + "/";
  107. }
  108. if ( libraryPathPrefix === resolvedEmbeddedBy ) {
  109. log.verbose(" component's 'sap.app/embeddedBy' property points to library, list it as 'embedded'");
  110. return true;
  111. } else {
  112. log.verbose(
  113. " component's 'sap.app/embeddedBy' points to '%s', don't list it as 'embedded'", resolvedEmbeddedBy
  114. );
  115. return false;
  116. }
  117. };
  118. /**
  119. * Retrieves the manifest path of a subcomponent
  120. *
  121. * @param {string} filePath path to the manifest, e.g. "lib/x/manifest.json"
  122. * @param {string} subPath relative sub path, e.g. "sub"
  123. * @returns {string} manifest path, e.g. "lib/x/sub/manifest.json"
  124. */
  125. const getManifestPath = (filePath, subPath) => {
  126. return posixPath.resolve(posixPath.dirname(filePath), subPath, "manifest.json");
  127. };
  128. /**
  129. * Represents dependency information for a library.
  130. * Dependencies can be retrieved using <code>#getResolvedLibraries</code>
  131. * and with that are resolved recursively
  132. */
  133. class DependencyInfo {
  134. /**
  135. *
  136. * @param {ManifestLibraries} libs
  137. * @param {string} name library name, e.g. "lib.x"
  138. */
  139. constructor(libs, name) {
  140. this.libs = libs;
  141. this.name = name;
  142. }
  143. /**
  144. * Add library to libsResolved and if already present
  145. * merge lazy property
  146. *
  147. * @param {string} libName library name, e.g. "lib.x"
  148. * @param {boolean} lazy
  149. * @returns {{lazy: boolean}} the added library
  150. */
  151. addResolvedLibDependency(libName, lazy) {
  152. let alreadyResolved = this._libsResolved[libName];
  153. if (!alreadyResolved) {
  154. alreadyResolved = Object.create(null);
  155. if (lazy) {
  156. alreadyResolved.lazy = true;
  157. }
  158. this._libsResolved[libName] = alreadyResolved;
  159. } else {
  160. // siblings if sibling is eager only if one other sibling eager
  161. alreadyResolved.lazy = alreadyResolved.lazy && lazy;
  162. }
  163. return alreadyResolved;
  164. }
  165. /**
  166. * Resolves dependencies recursively and retrieves them with
  167. * - resolved siblings a lazy and a eager dependency becomes eager
  168. * - resolved children become lazy if their parent is lazy
  169. *
  170. * @param {Map<string,DependencyInfo>} dependencyInfoMap
  171. * @returns {ManifestLibraries} resolved libraries
  172. */
  173. getResolvedLibraries(dependencyInfoMap) {
  174. if (!this._libsResolved) {
  175. // early set if there is a potential cycle
  176. this._libsResolved = Object.create(null);
  177. if (!this.libs) {
  178. return this._libsResolved;
  179. }
  180. for (const [libName, libValue] of Object.entries(this.libs)) {
  181. const lazy = libValue.lazy;
  182. const dependencyInfoObjectAdded = this.addResolvedLibDependency(libName, lazy);
  183. const dependencyInfo = dependencyInfoMap.get(libName);
  184. if (dependencyInfo) {
  185. const childLibsResolved = dependencyInfo.getResolvedLibraries(dependencyInfoMap);
  186. // children if parent is lazy children become lazy
  187. for (const [resolvedLibName, resolvedLib] of Object.entries(childLibsResolved)) {
  188. this.addResolvedLibDependency(resolvedLibName,
  189. resolvedLib.lazy || dependencyInfoObjectAdded.lazy);
  190. }
  191. } else {
  192. log.info(`Cannot find dependency '${libName}' `+
  193. `defined in the manifest.json or .library file of project '${this.name}'. ` +
  194. "This might prevent some UI5 runtime performance optimizations from taking effect. " +
  195. "Please double check your project's dependency configuration.");
  196. }
  197. }
  198. }
  199. return this._libsResolved;
  200. }
  201. }
  202. /**
  203. * Sorts the keys of a given object
  204. *
  205. * @param {object} obj the object
  206. * @returns {object} the object with sorted keys
  207. */
  208. const sortObjectKeys = (obj) => {
  209. const sortedObject = {};
  210. const keys = Object.keys(obj);
  211. keys.sort();
  212. keys.forEach((key) => {
  213. sortedObject[key] = obj[key];
  214. });
  215. return sortedObject;
  216. };
  217. /**
  218. * Builds the manifestHints object from the dependencyInfo
  219. *
  220. * @param {DependencyInfo} dependencyInfo
  221. * @param {Map<string, DependencyInfo>} dependencyInfoMap
  222. * @returns {{dependencies: {libs: ManifestLibraries}}} manifestHints
  223. */
  224. const getManifestHints = (dependencyInfo, dependencyInfoMap) => {
  225. if (dependencyInfo) {
  226. const libsResolved = dependencyInfo.getResolvedLibraries(dependencyInfoMap);
  227. if (libsResolved && Object.keys(libsResolved).length) {
  228. return {
  229. dependencies: {
  230. libs: sortObjectKeys(libsResolved)
  231. }
  232. };
  233. }
  234. }
  235. };
  236. /**
  237. * Common type for Library and Component
  238. * embeds and bundled components make only sense for library
  239. *
  240. * @typedef {object} ArtifactInfo
  241. * @property {string} componentName The library name, e.g. "lib.x"
  242. * @property {Set<string>} bundledComponents The embedded components which have an embeddedBy reference to the library
  243. * @property {DependencyInfo} dependencyInfo The dependency info object
  244. * @property {ArtifactInfo[]} embeds The embedded artifact infos
  245. */
  246. /**
  247. * Processes the manifest and creates a ManifestInfo and an ArtifactInfo.
  248. *
  249. * @param {module:@ui5/fs.Resource} libraryManifest
  250. * @param {string} [name] library name, if not provided using the ManifestInfo's id
  251. * @returns {Promise<{manifestInfo: ManifestInfo, libraryArtifactInfo: ArtifactInfo}>}
  252. */
  253. async function processManifestAndGetArtifactInfo(libraryManifest, name) {
  254. const manifestInfo = await processManifest(libraryManifest);
  255. name = name || manifestInfo.id;
  256. const libraryArtifactInfo = Object.create(null);
  257. libraryArtifactInfo.componentName = name;
  258. libraryArtifactInfo.dependencyInfo = new DependencyInfo(manifestInfo.libs, name);
  259. return {manifestInfo, libraryArtifactInfo};
  260. }
  261. /**
  262. * Processes the library info and fills the maps <code>dependencyInfoMap</code> and <code>embeddedInfoMap</code>.
  263. *
  264. * @param {LibraryInfo} libraryInfo
  265. * @returns {Promise<ArtifactInfo|undefined>}
  266. */
  267. const processLibraryInfo = async (libraryInfo) => {
  268. if (!libraryInfo.libraryManifest) {
  269. log.verbose(
  270. `Cannot add meta information for library '${libraryInfo.name}'. The manifest.json file cannot be found`);
  271. return;
  272. }
  273. const {manifestInfo, libraryArtifactInfo} =
  274. await processManifestAndGetArtifactInfo(libraryInfo.libraryManifest, libraryInfo.name);
  275. const bundledComponents = new Set();
  276. libraryArtifactInfo.bundledComponents = bundledComponents;
  277. const embeds = manifestInfo.embeds||[]; // e.g. ["sub"]
  278. // filter only embedded manifests
  279. const embeddedPaths = embeds.map((embed) => {
  280. return getManifestPath(libraryInfo.libraryManifest.getPath(), embed);
  281. });
  282. // e.g. manifest resource with lib/x/sub/manifest.json
  283. let embeddedManifests = libraryInfo.embeddedManifests || [];
  284. embeddedManifests = embeddedManifests.filter((manifestResource) => {
  285. return embeddedPaths.includes(manifestResource.getPath());
  286. });
  287. // get all embedded manifests
  288. const embeddedManifestPromises = embeddedManifests.map(async (embeddedManifest) => {
  289. const {manifestInfo: embeddedManifestInfo, libraryArtifactInfo: embeddedArtifactInfo} =
  290. await processManifestAndGetArtifactInfo(embeddedManifest);
  291. const componentName = embeddedManifestInfo.id;
  292. const embeddedManifestDirName = posixPath.dirname(embeddedManifest.getPath());
  293. const libraryManifestDirName = posixPath.dirname(libraryInfo.libraryManifest.getPath());
  294. if (isBundledWithLibrary(embeddedManifestInfo.embeddedBy, embeddedManifestDirName,
  295. libraryManifestDirName + "/")) {
  296. bundledComponents.add(componentName);
  297. }
  298. return embeddedArtifactInfo;
  299. });
  300. const embeddedArtifactInfos = await Promise.all(embeddedManifestPromises);
  301. libraryArtifactInfo.embeds = embeddedArtifactInfos;
  302. return libraryArtifactInfo;
  303. };
  304. /**
  305. * Library Info
  306. *
  307. * contains information about the name and the version of the library and its manifest, as well as the nested manifests.
  308. *
  309. * @typedef {object} LibraryInfo
  310. * @property {string} name The library name, e.g. "lib.x"
  311. * @property {string} version The library version, e.g. "1.0.0"
  312. * @property {module:@ui5/fs.Resource} libraryManifest library manifest resource,
  313. * e.g. resource with path "lib/x/manifest.json"
  314. * @property {module:@ui5/fs.Resource[]} embeddedManifests list of embedded manifest resources,
  315. * e.g. resource with path "lib/x/sub/manifest.json"
  316. * @public
  317. */
  318. /**
  319. * Creates sap-ui-version.json.
  320. *
  321. * @public
  322. * @alias module:@ui5/builder.processors.versionInfoGenerator
  323. * @param {object} parameters Parameters
  324. * @param {object} parameters.options Options
  325. * @param {string} parameters.options.rootProjectName Name of the root project
  326. * @param {string} parameters.options.rootProjectVersion Version of the root project
  327. * @param {LibraryInfo[]} parameters.options.libraryInfos Array of objects representing libraries,
  328. * e.g. <pre>
  329. * {
  330. * name: "lib.x",
  331. * version: "1.0.0",
  332. * libraryManifest: module:@ui5/fs.Resource,
  333. * embeddedManifests: module:@ui5/fs.Resource[]
  334. * }
  335. * </pre>
  336. * @returns {Promise<module:@ui5/fs.Resource[]>} Promise resolving with an array containing the versionInfo resource
  337. */
  338. module.exports = async function({options}) {
  339. if (!options.rootProjectName || options.rootProjectVersion === undefined || options.libraryInfos === undefined) {
  340. throw new Error("[versionInfoGenerator]: Missing options parameters");
  341. }
  342. const buildTimestamp = getTimestamp();
  343. /**
  344. * componentName to dependency info
  345. *
  346. * @type {Map<string, DependencyInfo>}
  347. */
  348. const dependencyInfoMap = new Map();
  349. // process library infos
  350. const libraryInfosProcessPromises = options.libraryInfos.map((libraryInfo) => {
  351. return processLibraryInfo(libraryInfo);
  352. });
  353. let artifactInfos = await Promise.all(libraryInfosProcessPromises);
  354. artifactInfos = artifactInfos.filter(Boolean);
  355. // fill dependencyInfoMap
  356. artifactInfos.forEach((artifactInfo) => {
  357. dependencyInfoMap.set(artifactInfo.componentName, artifactInfo.dependencyInfo);
  358. });
  359. const libraries = options.libraryInfos.map((libraryInfo) => {
  360. const library = {
  361. name: libraryInfo.name,
  362. version: libraryInfo.version,
  363. buildTimestamp: buildTimestamp,
  364. scmRevision: ""// TODO: insert current library scm revision here
  365. };
  366. const dependencyInfo = dependencyInfoMap.get(libraryInfo.name);
  367. const manifestHints = getManifestHints(dependencyInfo, dependencyInfoMap);
  368. if (manifestHints) {
  369. library.manifestHints = manifestHints;
  370. }
  371. return library;
  372. });
  373. // sort libraries alphabetically
  374. libraries.sort((a, b) => {
  375. return a.name.localeCompare(b.name);
  376. });
  377. // components
  378. let components;
  379. artifactInfos.forEach((artifactInfo) => {
  380. artifactInfo.embeds.forEach((embeddedArtifactInfo) => {
  381. const componentObject = {};
  382. const bundledComponents = artifactInfo.bundledComponents;
  383. const componentName = embeddedArtifactInfo.componentName;
  384. if (!bundledComponents.has(componentName)) {
  385. componentObject.hasOwnPreload = true;
  386. }
  387. componentObject.library = artifactInfo.componentName;
  388. const manifestHints = getManifestHints(embeddedArtifactInfo.dependencyInfo, dependencyInfoMap);
  389. if (manifestHints) {
  390. componentObject.manifestHints = manifestHints;
  391. }
  392. components = components || {};
  393. components[componentName] = componentObject;
  394. });
  395. });
  396. // sort components alphabetically
  397. components = components && sortObjectKeys(components);
  398. const versionJson = {
  399. name: options.rootProjectName,
  400. version: options.rootProjectVersion, // TODO: insert current application version here
  401. buildTimestamp: buildTimestamp,
  402. scmRevision: "", // TODO: insert current application scm revision here
  403. // gav: "", // TODO: insert current application id + version here
  404. libraries,
  405. components
  406. };
  407. return [resourceFactory.createResource({
  408. path: "/resources/sap-ui-version.json",
  409. string: JSON.stringify(versionJson, null, "\t")
  410. })];
  411. };