UNPKG

@ui5/builder

Version:
470 lines (418 loc) 15.7 kB
import {getLogger} from "@ui5/logger"; const log = getLogger("builder:processors:versionInfoGenerator"); import {createResource} from "@ui5/fs/resourceFactory"; import posixPath from "node:path/posix"; /** * @public * @module @ui5/builder/processors/versionInfoGenerator */ function pad(v) { return String(v).padStart(2, "0"); } function getTimestamp() { const date = new Date(); const year = date.getFullYear(); const month = pad(date.getMonth() + 1); const day = pad(date.getDate()); const hours = pad(date.getHours()); const minutes = pad(date.getMinutes()); // yyyyMMddHHmm return year + month + day + hours + minutes; } /** * Manifest libraries as defined in the manifest.json file * * @typedef {Object<string, {lazy: boolean}>} ManifestLibraries * * sample: * <pre> * { * "sap.chart": { * "lazy": true * }, * "sap.f": { } * } * </pre> */ /** * Extracted information from a manifest's <code>sap.app</code> and <code>sap.ui5</code> sections. * * @typedef {object} ManifestInfo * * @property {string} id The library name, e.g. "lib.x" * @property {string} embeddedBy the library this component is embedded in, e.g. "lib.x" * @property {string[]} embeds the embedded component names, e.g. ["lib.x.sub"] * @property {module:@ui5/builder/processors/versionInfoGenerator~ManifestLibraries} libs the dependencies, e.g. * {"sap.chart":{"lazy": true}, "sap.f":{}} */ /** * Processes manifest resource and extracts information. * * @param {@ui5/fs/Resource} manifestResource * @returns {Promise<module:@ui5/builder/processors/versionInfoGenerator~ManifestInfo>} */ const processManifest = async (manifestResource) => { const manifestContent = await manifestResource.getString(); const manifestObject = JSON.parse(manifestContent); const manifestInfo = Object.create(null); // sap.ui5/dependencies is used for the "manifestHints/libs" if (manifestObject["sap.ui5"]) { const manifestDependencies = manifestObject["sap.ui5"]["dependencies"]; if (manifestDependencies && manifestDependencies.libs) { const libs = Object.create(null); for (const [libKey, libValue] of Object.entries(manifestDependencies.libs)) { libs[libKey] = Object.create(null); if (libValue.lazy) { libs[libKey].lazy = true; } } manifestInfo.libs = libs; } } // sap.app/embeds, sap.app/embeddedBy and sap.app/id is used for "components" if (manifestObject["sap.app"]) { const manifestEmbeds = manifestObject["sap.app"]["embeds"]; manifestInfo.embeds = manifestEmbeds; const manifestEmbeddedBy = manifestObject["sap.app"]["embeddedBy"]; manifestInfo.embeddedBy = manifestEmbeddedBy; const id = manifestObject["sap.app"]["id"]; manifestInfo.id = id; } return manifestInfo; }; /** * Checks if a component (componentPath) is bundled with the library (embeddedBy) * * @param {string} embeddedBy e.g. "../" * @param {string} componentPath e.g. "lib/x/sub" * @param {string} libraryPathPrefix e.g. "lib/x" * @returns {boolean} whether or not this component is bundled with the library */ const isBundledWithLibrary = (embeddedBy, componentPath, libraryPathPrefix) => { if (typeof embeddedBy === "undefined") { log.verbose(" Component doesn't declare 'sap.app/embeddedBy', don't list it as 'embedded'"); return false; } if (typeof embeddedBy !== "string") { log.error( ` Component '${componentPath}': property 'sap.app/embeddedBy' is of type '${typeof embeddedBy}' ` + `(expected 'string'), it won't be listed as 'embedded'`); return false; } if ( !embeddedBy.length ) { log.error( ` Component '${componentPath}': property 'sap.app/embeddedBy' has an empty string value ` + `(which is invalid), it won't be listed as 'embedded'` ); return false; } let resolvedEmbeddedBy = posixPath.resolve(componentPath, embeddedBy); if ( resolvedEmbeddedBy && !resolvedEmbeddedBy.endsWith("/") ) { resolvedEmbeddedBy = resolvedEmbeddedBy + "/"; } if ( libraryPathPrefix === resolvedEmbeddedBy ) { log.verbose(" Component's 'sap.app/embeddedBy' property points to library, list it as 'embedded'"); return true; } else { log.verbose( ` Component's 'sap.app/embeddedBy' points to '${resolvedEmbeddedBy}', don't list it as 'embedded'`); return false; } }; /** * Retrieves the manifest path of a subcomponent * * @param {string} filePath path to the manifest, e.g. "lib/x/manifest.json" * @param {string} subPath relative sub path, e.g. "sub" * @returns {string} manifest path, e.g. "lib/x/sub/manifest.json" */ const getManifestPath = (filePath, subPath) => { return posixPath.resolve(posixPath.dirname(filePath), subPath, "manifest.json"); }; /** * Represents dependency information for a library. * Dependencies can be retrieved using <code>#getResolvedLibraries</code> * and with that are resolved recursively */ class DependencyInfo { /** * * @param {module:@ui5/builder/processors/versionInfoGenerator~ManifestLibraries} libs * @param {string} name library name, e.g. "lib.x" */ constructor(libs, name) { this.libs = libs; this.name = name; } /** * Add library to libsResolved and if already present * merge lazy property * * @param {string} libName library name, e.g. "lib.x" * @param {boolean} lazy * @returns {{lazy: boolean}} the added library */ addResolvedLibDependency(libName, lazy) { let alreadyResolved = this._libsResolved[libName]; if (!alreadyResolved) { alreadyResolved = Object.create(null); if (lazy) { alreadyResolved.lazy = true; } this._libsResolved[libName] = alreadyResolved; } else { // siblings if sibling is eager only if one other sibling eager alreadyResolved.lazy = alreadyResolved.lazy && lazy; } return alreadyResolved; } /** * Resolves dependencies recursively and retrieves them with * - resolved siblings a lazy and a eager dependency becomes eager * - resolved children become lazy if their parent is lazy * * @param {Map<string,DependencyInfo>} dependencyInfoMap * @returns {module:@ui5/builder/processors/versionInfoGenerator~ManifestLibraries} resolved libraries */ getResolvedLibraries(dependencyInfoMap) { if (!this._libsResolved) { // early set if there is a potential cycle this._libsResolved = Object.create(null); if (!this.libs) { return this._libsResolved; } for (const [libName, libValue] of Object.entries(this.libs)) { const lazy = libValue.lazy; const dependencyInfoObjectAdded = this.addResolvedLibDependency(libName, lazy); const dependencyInfo = dependencyInfoMap.get(libName); if (dependencyInfo) { const childLibsResolved = dependencyInfo.getResolvedLibraries(dependencyInfoMap); // children if parent is lazy children become lazy for (const [resolvedLibName, resolvedLib] of Object.entries(childLibsResolved)) { this.addResolvedLibDependency(resolvedLibName, resolvedLib.lazy || dependencyInfoObjectAdded.lazy); } } else { log.info(`Cannot find dependency '${libName}' `+ `defined in the manifest.json or .library file of project '${this.name}'. ` + "This might prevent some UI5 runtime performance optimizations from taking effect. " + "Please double check your project's dependency configuration."); } } } return this._libsResolved; } } /** * Sorts the keys of a given object * * @param {object} obj the object * @returns {object} the object with sorted keys */ const sortObjectKeys = (obj) => { const sortedObject = Object.create(null); const keys = Object.keys(obj); keys.sort(); keys.forEach((key) => { sortedObject[key] = obj[key]; }); return sortedObject; }; /** * Builds the manifestHints object from the dependencyInfo * * @param {DependencyInfo} dependencyInfo * @param {Map<string, DependencyInfo>} dependencyInfoMap * @returns {{dependencies: {libs: ManifestLibraries}}} manifestHints */ const getManifestHints = (dependencyInfo, dependencyInfoMap) => { if (dependencyInfo) { const libsResolved = dependencyInfo.getResolvedLibraries(dependencyInfoMap); if (libsResolved && Object.keys(libsResolved).length) { return { dependencies: { libs: sortObjectKeys(libsResolved) } }; } } }; /** * Common type for Library and Component * embeds and bundled components make only sense for library * * @typedef {object} ArtifactInfo * @property {string} componentName The library name, e.g. "lib.x" * @property {Set<string>} bundledComponents The embedded components which have an embeddedBy reference to the library * @property {DependencyInfo} dependencyInfo The dependency info object * @property {module:@ui5/builder/processors/versionInfoGenerator~ArtifactInfo[]} embeds The embedded artifact infos */ /** * Processes the manifest and creates a ManifestInfo and an ArtifactInfo. * * @param {@ui5/fs/Resource} libraryManifest * @param {string} [name] library name, if not provided using the ManifestInfo's id * @returns {Promise<{manifestInfo: ManifestInfo, libraryArtifactInfo: ArtifactInfo}>} */ async function processManifestAndGetArtifactInfo(libraryManifest, name) { const manifestInfo = await processManifest(libraryManifest); name = name || manifestInfo.id; const libraryArtifactInfo = Object.create(null); libraryArtifactInfo.componentName = name; libraryArtifactInfo.dependencyInfo = new DependencyInfo(manifestInfo.libs, name); return {manifestInfo, libraryArtifactInfo}; } /** * Processes the library info and fills the maps <code>dependencyInfoMap</code> and <code>embeddedInfoMap</code>. * * @param {module:@ui5/builder/processors/versionInfoGenerator~LibraryInfo} libraryInfo * @returns {Promise<module:@ui5/builder/processors/versionInfoGenerator~ArtifactInfo|undefined>} */ const processLibraryInfo = async (libraryInfo) => { if (!libraryInfo.libraryManifest) { log.verbose( `Cannot add meta information for library '${libraryInfo.name}'. The manifest.json file cannot be found`); return; } const {manifestInfo, libraryArtifactInfo} = await processManifestAndGetArtifactInfo(libraryInfo.libraryManifest, libraryInfo.name); const bundledComponents = new Set(); libraryArtifactInfo.bundledComponents = bundledComponents; const embeds = manifestInfo.embeds||[]; // e.g. ["sub"] // filter only embedded manifests const embeddedPaths = embeds.map((embed) => { return getManifestPath(libraryInfo.libraryManifest.getPath(), embed); }); // e.g. manifest resource with lib/x/sub/manifest.json let embeddedManifests = libraryInfo.embeddedManifests || []; embeddedManifests = embeddedManifests.filter((manifestResource) => { return embeddedPaths.includes(manifestResource.getPath()); }); // get all embedded manifests const embeddedManifestPromises = embeddedManifests.map(async (embeddedManifest) => { const {manifestInfo: embeddedManifestInfo, libraryArtifactInfo: embeddedArtifactInfo} = await processManifestAndGetArtifactInfo(embeddedManifest); const componentName = embeddedManifestInfo.id; const embeddedManifestDirName = posixPath.dirname(embeddedManifest.getPath()); const libraryManifestDirName = posixPath.dirname(libraryInfo.libraryManifest.getPath()); if (isBundledWithLibrary(embeddedManifestInfo.embeddedBy, embeddedManifestDirName, libraryManifestDirName + "/")) { bundledComponents.add(componentName); } return embeddedArtifactInfo; }); const embeddedArtifactInfos = await Promise.all(embeddedManifestPromises); libraryArtifactInfo.embeds = embeddedArtifactInfos; return libraryArtifactInfo; }; /** * Library Info * * contains information about the name and the version of the library and its manifest, as well as the nested manifests. * * @public * @typedef {object} LibraryInfo * @property {string} name The library name, e.g. "lib.x" * @property {string} version The library version, e.g. "1.0.0" * @property {@ui5/fs/Resource} libraryManifest library manifest resource, * e.g. resource with path "lib/x/manifest.json" * @property {@ui5/fs/Resource[]} embeddedManifests list of embedded manifest resources, * e.g. resource with path "lib/x/sub/manifest.json" */ /** * Creates sap-ui-version.json. * * @public * @function default * @static * * @param {object} parameters Parameters * @param {object} parameters.options Options * @param {string} parameters.options.rootProjectName Name of the root project * @param {string} parameters.options.rootProjectVersion Version of the root project * @param {module:@ui5/builder/processors/versionInfoGenerator~LibraryInfo[]} parameters.options.libraryInfos Array of * objects representing libraries, * e.g. <pre> * { * name: "lib.x", * version: "1.0.0", * libraryManifest: @ui5/fs/Resource, * embeddedManifests: @ui5/fs/Resource[] * } * </pre> * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving with an array containing the versionInfo resource */ export default async function({options}) { if (!options.rootProjectName || options.rootProjectVersion === undefined || options.libraryInfos === undefined) { throw new Error("[versionInfoGenerator]: Missing options parameters"); } const buildTimestamp = getTimestamp(); /** * componentName to dependency info * * @type {Map<string, DependencyInfo>} */ const dependencyInfoMap = new Map(); // process library infos const libraryInfosProcessPromises = options.libraryInfos.map((libraryInfo) => { return processLibraryInfo(libraryInfo); }); let artifactInfos = await Promise.all(libraryInfosProcessPromises); artifactInfos = artifactInfos.filter(Boolean); // fill dependencyInfoMap artifactInfos.forEach((artifactInfo) => { dependencyInfoMap.set(artifactInfo.componentName, artifactInfo.dependencyInfo); }); const libraries = options.libraryInfos.map((libraryInfo) => { const library = { name: libraryInfo.name, version: libraryInfo.version, buildTimestamp: buildTimestamp, scmRevision: ""// TODO: insert current library scm revision here }; const dependencyInfo = dependencyInfoMap.get(libraryInfo.name); const manifestHints = getManifestHints(dependencyInfo, dependencyInfoMap); if (manifestHints) { library.manifestHints = manifestHints; } return library; }); // sort libraries alphabetically libraries.sort((a, b) => { return a.name.localeCompare(b.name); }); // components let components; artifactInfos.forEach((artifactInfo) => { artifactInfo.embeds.forEach((embeddedArtifactInfo) => { const componentObject = Object.create(null); const bundledComponents = artifactInfo.bundledComponents; const componentName = embeddedArtifactInfo.componentName; if (!bundledComponents.has(componentName)) { componentObject.hasOwnPreload = true; } componentObject.library = artifactInfo.componentName; const manifestHints = getManifestHints(embeddedArtifactInfo.dependencyInfo, dependencyInfoMap); if (manifestHints) { componentObject.manifestHints = manifestHints; } components = components || Object.create(null); components[componentName] = componentObject; }); }); // sort components alphabetically components = components && sortObjectKeys(components); const versionJson = { name: options.rootProjectName, version: options.rootProjectVersion, // TODO: insert current application version here buildTimestamp: buildTimestamp, scmRevision: "", // TODO: insert current application scm revision here // gav: "", // TODO: insert current application id + version here libraries, components }; return [createResource({ path: "/resources/sap-ui-version.json", string: JSON.stringify(versionJson, null, "\t") })]; }