diff --git a/lib/builder/builder.js b/lib/builder/builder.js index 4a74bc029..9e45cd893 100644 --- a/lib/builder/builder.js +++ b/lib/builder/builder.js @@ -75,6 +75,8 @@ function composeTaskList({dev, selfContained, jsdoc, includedTasks, excludedTask selectedTasks.generateLibraryPreload = false; } + // TODO 3.0: exclude generateVersionInfo if not --all is used + if (jsdoc) { // Include JSDoc tasks selectedTasks.generateJsdoc = true; diff --git a/lib/processors/versionInfoGenerator.js b/lib/processors/versionInfoGenerator.js index 39189b5d8..052e65e8a 100644 --- a/lib/processors/versionInfoGenerator.js +++ b/lib/processors/versionInfoGenerator.js @@ -1,4 +1,6 @@ +const log = require("@ui5/logger").getLogger("builder:processors:versionInfoGenerator"); const resourceFactory = require("@ui5/fs").resourceFactory; +const posixPath = require("path").posix; function pad(v) { return String(v).padStart(2, "0"); @@ -14,6 +16,338 @@ function getTimestamp() { return year + month + day + hours + minutes; } +/** + * Manifest libraries as defined in the manifest.json file + * + * @typedef {object} ManifestLibraries + * + * sample: + *
+ * {
+ * 	"sap.chart": {
+ * 		"lazy": true
+ * 	},
+ * 	"sap.f": { }
+ * }
+ * 
+ */ + +/** + * Extracted information from a manifest's sap.app and sap.ui5 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 {ManifestLibraries} libs the dependencies, e.g. {"sap.chart":{"lazy": true}, "sap.f":{}} + */ + + +/** + * Processes manifest resource and extracts information. + * + * @param {module:@ui5/fs.Resource} manifestResource + * @returns {Promise} + */ +const processManifest = async (manifestResource) => { + const manifestContent = await manifestResource.getString(); + const manifestObject = JSON.parse(manifestContent); + const manifestInfo = {}; + + // 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 = {}; + for (const [libKey, libValue] of Object.entries(manifestDependencies.libs)) { + libs[libKey] = {}; + 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 '%s': property 'sap.app/embeddedBy' is of type '%s' (expected 'string'), " + + "it won't be listed as 'embedded'", componentPath, typeof embeddedBy + ); + return false; + } + if ( !embeddedBy.length ) { + log.error( + " component '%s': property 'sap.app/embeddedBy' has an empty string value (which is invalid), " + + "it won't be listed as 'embedded'", componentPath + ); + 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 '%s', don't list it as 'embedded'", resolvedEmbeddedBy + ); + 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 #getResolvedLibraries + * and with that are resolved recursively + */ +class DependencyInfo { + /** + * + * @param {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} dependencyInfoMap + * @returns {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 = {}; + 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} 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 makes only sense for library + * + * @typedef {object} ArtifactInfo + * @property {string} componentName The library name, e.g. "lib.x" + * @property {Set} bundledComponents The embedded components which have a embeddedBy reference to the library + * @property {DependencyInfo} dependencyInfo The dependency info object + * @property {ArtifactInfo[]} embeds The embedded artifact infos + */ + + +/** + * Processes the manifest and creates a ManifestInfo and an ArtifactInfo. + * + * @param {module:@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 dependencyInfoMap and embeddedInfoMap. + * + * @param {LibraryInfo} libraryInfo + * @returns {Promise} + */ +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. + * + * @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 {module:@ui5/fs.Resource} libraryManifest library manifest resource, + * e.g. resource with path "lib/x/manifest.json" + * @property {module:@ui5/fs.Resource[]} embeddedManifests list of embedded manifest resources, + * e.g. resource with path "lib/x/sub/manifest.json" + * @public + */ + /** * Creates sap-ui-version.json. * @@ -23,9 +357,16 @@ function getTimestamp() { * @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 {Array} parameters.options.libraryInfos Array of objects representing libraries, - * e.g. {name: "library.xy", version: "1.0.0"} - * @returns {Promise} Promise resolving with an array containing the versioninfo resource + * @param {LibraryInfo[]} parameters.options.libraryInfos Array of objects representing libraries, + * e.g.
+ *   {
+ *      name: "lib.x",
+ *      version: "1.0.0",
+ *      libraryManifest: module:@ui5/fs.Resource,
+ *      embeddedManifests: module:@ui5/fs.Resource[]
+ *   }
+ * 
+ * @returns {Promise} Promise resolving with an array containing the versionInfo resource */ module.exports = async function({options}) { @@ -34,20 +375,82 @@ module.exports = async function({options}) { } const buildTimestamp = getTimestamp(); + + /** + * componentName to dependency info + * + * @type {Map} + */ + 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 = { + library: artifactInfo.componentName + }; + const componentName = embeddedArtifactInfo.componentName; + const manifestHints = getManifestHints(embeddedArtifactInfo.dependencyInfo, dependencyInfoMap); + if (manifestHints) { + componentObject.manifestHints = manifestHints; + } + const bundledComponents = artifactInfo.bundledComponents; + if (bundledComponents.has(componentName)) { + componentObject.hasOwnPreload = true; + } + components = components || {}; + 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: options.libraryInfos.map(function(libraryInfo) { - return { - name: libraryInfo.name, - version: libraryInfo.version, - buildTimestamp: buildTimestamp, - scmRevision: "" // TODO: insert current library scm revision here - }; - }) + libraries, + components }; return [resourceFactory.createResource({ diff --git a/lib/tasks/generateVersionInfo.js b/lib/tasks/generateVersionInfo.js index f60a69ced..03e34e309 100644 --- a/lib/tasks/generateVersionInfo.js +++ b/lib/tasks/generateVersionInfo.js @@ -1,5 +1,7 @@ const versionInfoGenerator = require("../processors/versionInfoGenerator"); +const MANIFEST_JSON = "manifest.json"; + /** * Task to create sap-ui-version.json * @@ -13,23 +15,35 @@ const versionInfoGenerator = require("../processors/versionInfoGenerator"); * @param {object} parameters.options.rootProject DuplexCollection to read and write files * @returns {Promise} Promise resolving with undefined once data has been written */ -module.exports = function({workspace, dependencies, options: {rootProject, pattern}}) { - return dependencies.byGlob(pattern) - .then((resources) => { - return versionInfoGenerator({ - options: { - rootProjectName: rootProject.metadata.name, - rootProjectVersion: rootProject.version, - libraryInfos: resources.map((dotLibResource) => { - return { - name: dotLibResource._project.metadata.name, - version: dotLibResource._project.version - }; - }) - } +module.exports = async ({workspace, dependencies, options: {rootProject, pattern}}) => { + const resources = await dependencies.byGlob(pattern); + + const libraryInfosPromises = resources.map((dotLibResource) => { + const namespace = dotLibResource._project.metadata.namespace; + // pass all required resources to the processor + // the processor will then filter + return dependencies.byGlob(`/resources/${namespace}/**/${MANIFEST_JSON}`).then((manifestResources) => { + const libraryManifest = manifestResources.find((manifestResource) => { + return manifestResource.getPath() === `/resources/${namespace}/${MANIFEST_JSON}`; }); - }) - .then(([versionInfoResource]) => { - return workspace.write(versionInfoResource); + const embeddedManifests = + manifestResources.filter((manifestResource) => manifestResource !== libraryManifest); + return { + libraryManifest, + embeddedManifests, + name: dotLibResource._project.metadata.name, + version: dotLibResource._project.version + }; }); + }); + const libraryInfos = await Promise.all(libraryInfosPromises); + + const [versionInfoResource] = await versionInfoGenerator({ + options: { + rootProjectName: rootProject.metadata.name, + rootProjectVersion: rootProject.version, + libraryInfos + } + }); + return workspace.write(versionInfoResource); }; diff --git a/test/lib/processors/versionInfoGenerator.js b/test/lib/processors/versionInfoGenerator.js new file mode 100644 index 000000000..6f09b14f5 --- /dev/null +++ b/test/lib/processors/versionInfoGenerator.js @@ -0,0 +1,368 @@ +const test = require("ava"); +const sinon = require("sinon"); + +const mock = require("mock-require"); +const logger = require("@ui5/logger"); + +let versionInfoGenerator = require("../../../lib/processors/versionInfoGenerator"); + + +test("versionInfoGenerator missing parameters", async (t) => { + const error = await t.throwsAsync(versionInfoGenerator({options: {}})); + t.deepEqual(error.message, "[versionInfoGenerator]: Missing options parameters"); +}); + +test.beforeEach((t) => { + t.context.warnLogStub = sinon.stub(); + t.context.infoLogStub = sinon.stub(); + t.context.verboseLogStub = sinon.stub(); + sinon.stub(logger, "getLogger").returns({ + warn: t.context.warnLogStub, + info: t.context.infoLogStub, + verbose: t.context.verboseLogStub, + isLevelEnabled: () => true + }); + versionInfoGenerator = mock.reRequire("../../../lib/processors/versionInfoGenerator"); +}); + +test.afterEach.always((t) => { + mock.stopAll(); + sinon.restore(); +}); + +const assertVersionInfoContent = (t, oExpectedVersionInfo, sActualContent) => { + const currentVersionInfo = JSON.parse(sActualContent); + + t.is(currentVersionInfo.buildTimestamp.length, 12, "Timestamp should have length of 12 (yyyyMMddHHmm)"); + + delete currentVersionInfo.buildTimestamp; // removing to allow deep comparison + currentVersionInfo.libraries.forEach((lib) => { + t.is(lib.buildTimestamp.length, 12, "Timestamp should have length of 12 (yyyyMMddHHmm)"); + delete lib.buildTimestamp; // removing to allow deep comparison + }); + + + t.deepEqual(currentVersionInfo, oExpectedVersionInfo, "Correct content"); +}; + +test.serial("versionInfoGenerator empty libraryInfos parameter", async (t) => { + const versionInfos = await versionInfoGenerator({options: { + rootProjectName: "myname", rootProjectVersion: "1.33.7", libraryInfos: []}}); + + const resource = versionInfos[0]; + const result = await resource.getString(); + + const oExpected = { + "name": "myname", + "version": "1.33.7", + "scmRevision": "", + "libraries": [] + }; + assertVersionInfoContent(t, oExpected, result); +}); + + +test.serial("versionInfoGenerator simple library infos", async (t) => { + const options = { + rootProjectName: "myname", rootProjectVersion: "1.33.7", libraryInfos: [ + {name: "my.lib", version: "1.2.3"} + ]}; + const versionInfos = await versionInfoGenerator({options}); + + const resource = versionInfos[0]; + const result = await resource.getString(); + + const oExpected = { + "name": "myname", + "version": "1.33.7", + "scmRevision": "", + "libraries": [ + { + "name": "my.lib", + "version": "1.2.3", + "scmRevision": "" + } + ] + }; + assertVersionInfoContent(t, oExpected, result); + t.is(t.context.verboseLogStub.callCount, 1); + t.is(t.context.verboseLogStub.getCall(0).args[0], + "Cannot add meta information for library 'my.lib'. The manifest.json file cannot be found"); +}); + +test.serial("versionInfoGenerator manifest without libs", async (t) => { + const libAManifest = { + getPath: () => { + return "/resources/lib/a/manifest.json"; + }, + getString: async () => { + return JSON.stringify({ + "sap.app": { + "id": "lib.a", + "embeds": [] + }, + "sap.ui5": { + "dependencies": { + "minUI5Version": "1.84" + } + } + }); + } + }; + const libA = {name: "lib.a", version: "1.2.3", libraryManifest: libAManifest}; + + const options = { + rootProjectName: "myname", rootProjectVersion: "1.33.7", libraryInfos: [ + libA + ]}; + const versionInfos = await versionInfoGenerator({options}); + + const resource = versionInfos[0]; + const result = await resource.getString(); + + const oExpected = { + "name": "myname", + "version": "1.33.7", + "scmRevision": "", + "libraries": [ + { + "name": "lib.a", + "version": "1.2.3", + "scmRevision": "" + } + ] + }; + assertVersionInfoContent(t, oExpected, result); + t.is(t.context.infoLogStub.callCount, 0); + t.is(t.context.warnLogStub.callCount, 0); +}); + +test.serial("versionInfoGenerator library infos with dependencies", async (t) => { + const libAManifest = { + getPath: () => { + return "/resources/lib/a/manifest.json"; + }, + getString: async () => { + return JSON.stringify({ + "sap.app": { + "id": "lib.a", + "embeds": [] + }, + "sap.ui5": { + "dependencies": { + "minUI5Version": "1.84", + "libs": { + "my.dep": { + "minVersion": "1.84.0", + "lazy": false + } + } + } + } + }); + } + }; + const libA = {name: "lib.a", version: "1.2.3", libraryManifest: libAManifest}; + const myDepManifest = { + getPath: () => { + return "/resources/my/dep/manifest.json"; + }, + getString: async () => { + return JSON.stringify({ + "sap.app": { + "id": "my.dep", + "embeds": [] + }, + "sap.ui5": { + "dependencies": { + "minUI5Version": "1.84", + "libs": {} + } + } + }); + } + }; + const myDep = {name: "my.dep", version: "1.2.3", libraryManifest: myDepManifest}; + const options = { + rootProjectName: "myname", rootProjectVersion: "1.33.7", libraryInfos: [ + libA, myDep + ]}; + const versionInfos = await versionInfoGenerator({options}); + + const resource = versionInfos[0]; + const result = await resource.getString(); + + const oExpected = { + "name": "myname", + "version": "1.33.7", + "scmRevision": "", + "libraries": [ + { + "name": "lib.a", + "version": "1.2.3", + "scmRevision": "", + "manifestHints": { + "dependencies": { + "libs": { + "my.dep": {} + } + } + } + }, + { + "name": "my.dep", + "version": "1.2.3", + "scmRevision": "" + } + ] + }; + assertVersionInfoContent(t, oExpected, result); + t.is(t.context.infoLogStub.callCount, 0); + t.is(t.context.warnLogStub.callCount, 0); +}); + +test.serial("versionInfoGenerator library infos with embeds", async (t) => { + const libAManifest = { + getPath: () => { + return "/resources/lib/a/manifest.json"; + }, + getString: async () => { + return JSON.stringify({ + "sap.app": { + "id": "lib.a", + "embeds": ["sub"] + }, + "sap.ui5": { + "dependencies": { + "minUI5Version": "1.84", + "libs": {} + } + } + }); + } + }; + const subManifest = { + getPath: () => { + return "/resources/lib/a/sub/manifest.json"; + }, + getString: async () => { + return JSON.stringify({ + "sap.app": { + "id": "lib.a.sub", + "embeds": [] + }, + "sap.ui5": { + "dependencies": { + "minUI5Version": "1.84", + "libs": {} + } + } + }); + } + }; + const libA = {name: "lib.a", version: "1.2.3", libraryManifest: libAManifest, embeddedManifests: [subManifest]}; + + const options = { + rootProjectName: "myname", rootProjectVersion: "1.33.7", libraryInfos: [ + libA + ]}; + const versionInfos = await versionInfoGenerator({options}); + + const resource = versionInfos[0]; + const result = await resource.getString(); + + const oExpected = { + "name": "myname", + "version": "1.33.7", + "scmRevision": "", + "libraries": [ + { + "name": "lib.a", + "version": "1.2.3", + "scmRevision": "" + } + ], + "components": { + "lib.a.sub": { + "library": "lib.a" + } + } + }; + assertVersionInfoContent(t, oExpected, result); + t.is(t.context.infoLogStub.callCount, 0); + t.is(t.context.warnLogStub.callCount, 0); +}); + +test.serial("versionInfoGenerator library infos with embeds and embeddedBy (hasOwnPreload)", async (t) => { + const libAManifest = { + getPath: () => { + return "/resources/lib/a/manifest.json"; + }, + getString: async () => { + return JSON.stringify({ + "sap.app": { + "id": "lib.a", + "embeds": ["sub"] + }, + "sap.ui5": { + "dependencies": { + "minUI5Version": "1.84", + "libs": {} + } + } + }); + } + }; + const subManifest = { + getPath: () => { + return "/resources/lib/a/sub/manifest.json"; + }, + getString: async () => { + return JSON.stringify({ + "sap.app": { + "id": "lib.a.sub", + "embeds": [], + "embeddedBy": "../" + }, + "sap.ui5": { + "dependencies": { + "minUI5Version": "1.84", + "libs": {} + } + } + }); + } + }; + const libA = {name: "lib.a", version: "1.2.3", libraryManifest: libAManifest, embeddedManifests: [subManifest]}; + + const options = { + rootProjectName: "myname", rootProjectVersion: "1.33.7", libraryInfos: [ + libA + ]}; + const versionInfos = await versionInfoGenerator({options}); + + const resource = versionInfos[0]; + const result = await resource.getString(); + + const oExpected = { + "name": "myname", + "version": "1.33.7", + "scmRevision": "", + "libraries": [ + { + "name": "lib.a", + "version": "1.2.3", + "scmRevision": "" + } + ], + "components": { + "lib.a.sub": { + "hasOwnPreload": true, + "library": "lib.a" + } + } + }; + assertVersionInfoContent(t, oExpected, result); + t.is(t.context.infoLogStub.callCount, 0); + t.is(t.context.warnLogStub.callCount, 0); +}); diff --git a/test/lib/tasks/generateVersionInfo.js b/test/lib/tasks/generateVersionInfo.js index c09c7326b..4d8984363 100644 --- a/test/lib/tasks/generateVersionInfo.js +++ b/test/lib/tasks/generateVersionInfo.js @@ -1,9 +1,12 @@ const test = require("ava"); -const generateVersionInfo = require("../../../lib/tasks/generateVersionInfo"); +let generateVersionInfo = require("../../../lib/tasks/generateVersionInfo"); const path = require("path"); const ui5Fs = require("@ui5/fs"); const resourceFactory = ui5Fs.resourceFactory; +const sinon = require("sinon"); +const mock = require("mock-require"); +const logger = require("@ui5/logger"); function createWorkspace() { return resourceFactory.createAdapter({ @@ -25,16 +28,18 @@ function createWorkspace() { }); } -function createDependencies() { - return resourceFactory.createAdapter({ - fsBasePath: path.join(__dirname, "..", "..", "fixtures", "sap.ui.core-evo", "main", "src"), - virBasePath: "/resources", +function createDependencies(oOptions = { + virBasePath: "/resources", + fsBasePath: path.join(__dirname, "..", "..", "fixtures", "sap.ui.core-evo", "main", "src") +}) { + oOptions = Object.assign(oOptions, { project: { metadata: { name: "test.lib3" }, version: "3.0.0"} }); + return resourceFactory.createAdapter(oOptions); } async function createOptions(t, options) { @@ -58,9 +63,7 @@ async function createOptions(t, options) { return oOptions; } - -async function assertCreatedVersionInfo(t, oExpectedVersionInfo, options) { - const oOptions = await createOptions(t, options); +async function assertCreatedVersionInfo(t, oExpectedVersionInfo, oOptions) { await generateVersionInfo(oOptions); const resource = await oOptions.workspace.byPath("/resources/sap-ui-version.json"); @@ -79,10 +82,36 @@ async function assertCreatedVersionInfo(t, oExpectedVersionInfo, options) { t.is(lib.buildTimestamp.length, 12, "Timestamp should have length of 12 (yyyyMMddHHmm)"); delete lib.buildTimestamp; // removing to allow deep comparison }); + + currentVersionInfo.libraries.sort((libraryA, libraryB) => { + return libraryA.name.localeCompare(libraryB.name); + }); + t.deepEqual(currentVersionInfo, oExpectedVersionInfo, "Correct content"); } -test("integration: Library without i18n bundle file", async (t) => { +test.beforeEach((t) => { + t.context.verboseLogStub = sinon.stub(); + t.context.errorLogStub = sinon.stub(); + t.context.warnLogStub = sinon.stub(); + t.context.infoLogStub = sinon.stub(); + sinon.stub(logger, "getLogger").returns({ + verbose: t.context.verboseLogStub, + error: t.context.errorLogStub, + warn: t.context.warnLogStub, + info: t.context.infoLogStub, + isLevelEnabled: () => true + }); + mock.reRequire("../../../lib/processors/versionInfoGenerator"); + generateVersionInfo = mock.reRequire("../../../lib/tasks/generateVersionInfo"); +}); + +test.afterEach.always((t) => { + mock.stopAll(); + sinon.restore(); +}); + +test.serial("integration: Library without i18n bundle file", async (t) => { t.context.workspace = createWorkspace(); t.context.dependencies = createDependencies(); @@ -105,6 +134,7 @@ test("integration: Library without i18n bundle file", async (t) => { project: t.context.workspace._project })); + const oOptions = await createOptions(t); await assertCreatedVersionInfo(t, { "libraries": [{ "name": "test.lib3", @@ -114,10 +144,14 @@ test("integration: Library without i18n bundle file", async (t) => { "name": "myname", "scmRevision": "", "version": "1.33.7", - }); + }, oOptions); + + t.is(t.context.verboseLogStub.callCount, 1); + t.is(t.context.verboseLogStub.getCall(0).args[0], + "Cannot add meta information for library 'test.lib3'. The manifest.json file cannot be found"); }); -test("integration: Library without i18n bundle file failure", async (t) => { +test.serial("integration: Library without i18n bundle file failure", async (t) => { t.context.workspace = createWorkspace(); t.context.dependencies = createDependencies(); @@ -156,3 +190,989 @@ test("integration: Library without i18n bundle file failure", async (t) => { t.is(error.message, "[versionInfoGenerator]: Missing options parameters"); }); }); + +/** + * + * @param {string[]} names e.g. ["lib", "a"] + * @returns {{metadata: {name, namespace}}} + */ +const createProjectMetadata = (names) => { + return { + metadata: { + name: names.join("."), + namespace: names.join("/") + } + }; +}; + +/** + * + * @param {module:@ui5/fs.DuplexCollection} dependencies + * @param {module:@ui5/fs.resourceFactory} resourceFactory + * @param {string[]} names e.g. ["lib", "a"] + * @param {object[]} deps + * @param {string[]} [embeds] + * @param {string} [embeddedBy] + * @returns {Promise} + */ +const createManifestResource = async (dependencies, resourceFactory, names, deps, embeds, embeddedBy) => { + const content = { + "sap.app": { + "id": names.join("."), + "embeds": [] + }, + "sap.ui5": { + "dependencies": { + "minUI5Version": "1.84", + "libs": {} + } + } + }; + + const libs = {}; + deps.forEach((dep) => { + libs[dep.name] = { + "minVersion": "1.84.0" + }; + if (dep.lazy) { + libs[dep.name].lazy = true; + } + }); + content["sap.ui5"]["dependencies"]["libs"] = libs; + if (embeds !== undefined) { + content["sap.app"]["embeds"] = embeds; + } + if (embeddedBy !== undefined) { + content["sap.app"]["embeddedBy"] = embeddedBy; + } + await dependencies.write(resourceFactory.createResource({ + path: `/resources/${names.join("/")}/manifest.json`, + string: JSON.stringify(content, null, 2), + project: createProjectMetadata(names) + })); +}; + +/** + * @param {module:@ui5/fs.DuplexCollection} dependencies + * @param {module:@ui5/fs.resourceFactory} resourceFactory + * @param {string[]} names e.g. ["lib", "a"] + * @returns {Promise} + */ +async function createDotLibrary(dependencies, resourceFactory, names) { + await dependencies.write(resourceFactory.createResource({ + path: `/resources/${names.join("/")}/.library`, + string: ` + + + ${names.join(".")} + SAP SE + + 2.0.0 + + Library ${names.slice(1).join(".").toUpperCase()} + + `, + project: createProjectMetadata(names) + })); +} + +/** + * + * @param {module:@ui5/fs.DuplexCollection} dependencies + * @param {module:@ui5/fs.resourceFactory} resourceFactory + * @param {string[]} names e.g. ["lib", "a"] + * @param {object[]} deps + * @param {string[]} [embeds] + */ +const createResources = async (dependencies, resourceFactory, names, deps, embeds) => { + await createDotLibrary(dependencies, resourceFactory, names); + await createManifestResource(dependencies, resourceFactory, names, deps, embeds); +}; + +test.serial("integration: sibling eager to lazy", async (t) => { + const workspace = createWorkspace(); + + await createDotLibrary(workspace, resourceFactory, ["test", "lib"]); + + // input + // lib.a => lib.b, lib.c + // lib.b => lib.c (true) + // lib.c => + + // expected outcome + // lib.a => lib.b, lib.c + // lib.b => lib.c (true) + // lib.c => + + // dependencies + const dependencies = createDependencies({virBasePath: "/"}); + + // lib.a + await createResources(dependencies, resourceFactory, ["lib", "a"], [{name: "lib.b"}, {name: "lib.c"}]); + + // lib.b + await createResources(dependencies, resourceFactory, ["lib", "b"], [{name: "lib.c", lazy: true}]); + + // lib.c + await createResources(dependencies, resourceFactory, ["lib", "c"], []); + + const oOptions = { + options: { + projectName: "Test Lib", + pattern: "/resources/**/.library", + rootProject: { + metadata: { + name: "myname" + }, + version: "1.33.7" + } + }, + workspace, + dependencies + }; + await assertCreatedVersionInfo(t, { + "name": "myname", + "scmRevision": "", + "version": "1.33.7", + "libraries": [{ + "name": "lib.a", + "scmRevision": "", + "manifestHints": { + "dependencies": { + "libs": { + "lib.b": {}, + "lib.c": {} + } + } + }, + }, + { + "name": "lib.b", + "scmRevision": "", + "manifestHints": { + "dependencies": { + "libs": { + "lib.c": { + "lazy": true + } + } + } + }, + }, + { + "name": "lib.c", + "scmRevision": "" + }], + }, oOptions); +}); + +test.serial("integration: sibling lazy to eager", async (t) => { + const workspace = createWorkspace(); + + await createDotLibrary(workspace, resourceFactory, ["test", "lib"]); + + // input + // lib.a => lib.b, lib.c (true) + // lib.b => lib.c + // lib.c => + + // expected outcome + // lib.a => lib.b, lib.c + // lib.b => lib.c + // lib.c => + + // dependencies + const dependencies = createDependencies({virBasePath: "/"}); + + // lib.a + await createResources(dependencies, resourceFactory, ["lib", "a"], + [{name: "lib.b"}, {name: "lib.c", lazy: true}]); + + // lib.b + await createResources(dependencies, resourceFactory, ["lib", "b"], [{name: "lib.c"}]); + + // lib.c + await createResources(dependencies, resourceFactory, ["lib", "c"], []); + + const oOptions = { + options: { + projectName: "Test Lib", + pattern: "/resources/**/.library", + rootProject: { + metadata: { + name: "myname" + }, + version: "1.33.7" + } + }, + workspace, + dependencies + }; + await assertCreatedVersionInfo(t, { + "name": "myname", + "scmRevision": "", + "version": "1.33.7", + "libraries": [{ + "name": "lib.a", + "scmRevision": "", + "manifestHints": { + "dependencies": { + "libs": { + "lib.b": {}, + "lib.c": {} + } + } + }, + }, + { + "name": "lib.b", + "scmRevision": "", + "manifestHints": { + "dependencies": { + "libs": { + "lib.c": {} + } + } + }, + }, + { + "name": "lib.c", + "scmRevision": "" + }], + }, oOptions); +}); + +test.serial("integration: children eager to lazy", async (t) => { + const workspace = createWorkspace(); + + await createDotLibrary(workspace, resourceFactory, ["test", "lib"]); + + // input + // lib.a => lib.b + // lib.b => lib.c (true) + // lib.c => + + // expected outcome + // lib.a => lib.b, lib.c (true) + // lib.b => lib.c (true) + // lib.c => + + // dependencies + const dependencies = createDependencies({virBasePath: "/"}); + + // lib.a + await createResources(dependencies, resourceFactory, ["lib", "a"], + [{name: "lib.b"}]); + + // lib.b + await createResources(dependencies, resourceFactory, ["lib", "b"], + [{name: "lib.c", lazy: true}]); + + // lib.c + await createResources(dependencies, resourceFactory, ["lib", "c"], []); + + const oOptions = { + options: { + projectName: "Test Lib", + pattern: "/resources/**/.library", + rootProject: { + metadata: { + name: "myname" + }, + version: "1.33.7" + } + }, + workspace, + dependencies + }; + await assertCreatedVersionInfo(t, { + "name": "myname", + "scmRevision": "", + "version": "1.33.7", + "libraries": [{ + "name": "lib.a", + "scmRevision": "", + "manifestHints": { + "dependencies": { + "libs": { + "lib.b": {}, + "lib.c": { + "lazy": true + } + } + } + }, + }, + { + "name": "lib.b", + "scmRevision": "", + "manifestHints": { + "dependencies": { + "libs": { + "lib.c": { + "lazy": true + } + } + } + }, + }, + { + "name": "lib.c", + "scmRevision": "" + }], + }, oOptions); +}); + +test.serial("integration: children lazy to eager", async (t) => { + const workspace = createWorkspace(); + + await createDotLibrary(workspace, resourceFactory, ["test", "lib"]); + + // input + // lib.a => lib.b (true) + // lib.b => lib.c + // lib.c => + + // expected outcome + // lib.a => lib.b (true), lib.c (true) + // lib.b => lib.c + // lib.c => + + // dependencies + const dependencies = createDependencies({virBasePath: "/"}); + + // lib.a + await createResources(dependencies, resourceFactory, ["lib", "a"], + [{name: "lib.b", lazy: true}]); + + // lib.b + await createResources(dependencies, resourceFactory, ["lib", "b"], + [{name: "lib.c"}]); + + // lib.c + await createResources(dependencies, resourceFactory, ["lib", "c"], []); + + const oOptions = { + options: { + projectName: "Test Lib", + pattern: "/resources/**/.library", + rootProject: { + metadata: { + name: "myname" + }, + version: "1.33.7" + } + }, + workspace, + dependencies + }; + await assertCreatedVersionInfo(t, { + "name": "myname", + "scmRevision": "", + "version": "1.33.7", + "libraries": [{ + "name": "lib.a", + "scmRevision": "", + "manifestHints": { + "dependencies": { + "libs": { + "lib.b": { + "lazy": true + }, + "lib.c": { + "lazy": true + } + } + } + }, + }, + { + "name": "lib.b", + "scmRevision": "", + "manifestHints": { + "dependencies": { + "libs": { + "lib.c": {} + } + } + }, + }, + { + "name": "lib.c", + "scmRevision": "" + }], + }, oOptions); +}); + +test.serial("integration: Library with dependencies and subcomponent complex scenario", async (t) => { + const workspace = createWorkspace(); + + await createDotLibrary(workspace, resourceFactory, ["test", "lib"]); + + // input + // lib.a => lib.b, lib.c + // lib.b => lib.c (true) + // lib.c => lib.d, lib.e (true) + // lib.d => lib.e + // lib.e => + // lib.a.sub.fold => lib.c + + // expected outcome + // lib.a => lib.b, lib.c, lib.d, lib.e + // lib.b => lib.c (true), lib.d (true), lib.e (true) + // lib.c => lib.d, lib.e + // lib.d => lib.e + // lib.e => + // lib.a.sub.fold => lib.c, lib.d, lib.e + + // dependencies + const dependencies = createDependencies({virBasePath: "/"}); + + // lib.a + const embeds = ["sub/fold"]; + await createResources(dependencies, resourceFactory, ["lib", "a"], [{name: "lib.b"}, {name: "lib.c"}], embeds); + // sub + await createManifestResource(dependencies, resourceFactory, ["lib", "a", "sub", "fold"], [{name: "lib.c"}]); + + // lib.b + await createResources(dependencies, resourceFactory, ["lib", "b"], [{name: "lib.c", lazy: true}]); + + // lib.c + await createResources(dependencies, resourceFactory, ["lib", "c"], [{name: "lib.d"}, {name: "lib.e", lazy: true}]); + + // lib.d + await createResources(dependencies, resourceFactory, ["lib", "d"], [{name: "lib.e"}]); + + // lib.e + await createResources(dependencies, resourceFactory, ["lib", "e"], []); + + const oOptions = { + options: { + projectName: "Test Lib", + pattern: "/resources/**/.library", + rootProject: { + metadata: { + name: "myname" + }, + version: "1.33.7" + } + }, + workspace, + dependencies + }; + await assertCreatedVersionInfo(t, { + "name": "myname", + "scmRevision": "", + "version": "1.33.7", + "libraries": [{ + "name": "lib.a", + "scmRevision": "", + "manifestHints": { + "dependencies": { + "libs": { + "lib.b": {}, + "lib.c": {}, + "lib.d": {}, + "lib.e": {} + } + } + }, + }, + { + "name": "lib.b", + "scmRevision": "", + "manifestHints": { + "dependencies": { + "libs": { + "lib.c": { + "lazy": true + }, + "lib.d": { + "lazy": true + }, + "lib.e": { + "lazy": true + } + } + } + }, + }, + { + "name": "lib.c", + "scmRevision": "", + "manifestHints": { + "dependencies": { + "libs": { + "lib.d": {}, + "lib.e": {} + } + } + }, + }, + { + "name": "lib.d", + "scmRevision": "", + "manifestHints": { + "dependencies": { + "libs": { + "lib.e": {} + } + } + }, + }, + { + "name": "lib.e", + "scmRevision": "", + }], + "components": { + "lib.a.sub.fold": { + "library": "lib.a", + "manifestHints": { + "dependencies": { + "libs": { + "lib.c": {}, + "lib.d": {}, + "lib.e": {} + } + } + } + } + }, + }, oOptions); +}); + +test.serial("integration: Library with dependencies and subcomponent bigger scenario", async (t) => { + const workspace = createWorkspace(); + + await createDotLibrary(workspace, resourceFactory, ["test", "lib"]); + + // input + // lib.a => lib.b, lib.c + // lib.b => lib.c (true) + // lib.c => lib.d, lib.e (true) + // lib.d => lib.e + // lib.e => + // lib.a.sub.fold => lib.c + + // expected outcome + // lib.a => lib.b, lib.c, lib.d, lib.e + // lib.b => lib.c (true), lib.d (true), lib.e (true) + // lib.c => lib.d, lib.e + // lib.d => lib.e + // lib.e => + // lib.a.sub.fold => lib.c, lib.d, lib.e + + // dependencies + const dependencies = createDependencies({virBasePath: "/"}); + + // lib.a + const embeds = ["sub/fold"]; + await createResources(dependencies, resourceFactory, ["lib", "a"], [{name: "lib.b"}, {name: "lib.c"}], embeds); + // sub + await createManifestResource(dependencies, resourceFactory, ["lib", "a", "sub", "fold"], [{name: "lib.c"}]); + + // lib.b + await createResources(dependencies, resourceFactory, ["lib", "b"], [{name: "lib.c", lazy: true}]); + + // lib.c + await createResources(dependencies, resourceFactory, ["lib", "c"], [{name: "lib.d"}, {name: "lib.e", lazy: true}]); + + // lib.d + await createResources(dependencies, resourceFactory, ["lib", "d"], [{name: "lib.e"}]); + + // lib.e + await createResources(dependencies, resourceFactory, ["lib", "e"], []); + + const oOptions = { + options: { + projectName: "Test Lib", + pattern: "/resources/**/.library", + rootProject: { + metadata: { + name: "myname" + }, + version: "1.33.7" + } + }, + workspace, + dependencies + }; + await assertCreatedVersionInfo(t, { + "name": "myname", + "scmRevision": "", + "version": "1.33.7", + "libraries": [{ + "name": "lib.a", + "scmRevision": "", + "manifestHints": { + "dependencies": { + "libs": { + "lib.b": {}, + "lib.c": {}, + "lib.d": {}, + "lib.e": {} + } + } + }, + }, + { + "name": "lib.b", + "scmRevision": "", + "manifestHints": { + "dependencies": { + "libs": { + "lib.c": { + "lazy": true + }, + "lib.d": { + "lazy": true + }, + "lib.e": { + "lazy": true + } + } + } + }, + }, + { + "name": "lib.c", + "scmRevision": "", + "manifestHints": { + "dependencies": { + "libs": { + "lib.d": {}, + "lib.e": {} + } + } + }, + }, + { + "name": "lib.d", + "scmRevision": "", + "manifestHints": { + "dependencies": { + "libs": { + "lib.e": {} + } + } + }, + }, + { + "name": "lib.e", + "scmRevision": "", + }], + "components": { + "lib.a.sub.fold": { + "library": "lib.a", + "manifestHints": { + "dependencies": { + "libs": { + "lib.c": {}, + "lib.d": {}, + "lib.e": {} + } + } + } + } + }, + }, oOptions); +}); + +test.serial("integration: Library without dependencies and embeds and embeddedBy", async (t) => { + const workspace = createWorkspace(); + + await createDotLibrary(workspace, resourceFactory, ["test", "lib"]); + + // dependencies + const dependencies = createDependencies({virBasePath: "/"}); + + // lib.a + const embeds = ["sub/fold"]; + await createResources(dependencies, resourceFactory, ["lib", "a"], [], embeds); + // sub + await createManifestResource(dependencies, resourceFactory, ["lib", "a", "sub", "fold"], [], + undefined, "../../"); + + const oOptions = { + options: { + projectName: "Test Lib", + pattern: "/resources/**/.library", + rootProject: { + metadata: { + name: "myname" + }, + version: "1.33.7" + } + }, + workspace, + dependencies + }; + await assertCreatedVersionInfo(t, { + "name": "myname", + "scmRevision": "", + "version": "1.33.7", + "libraries": [{ + "name": "lib.a", + "scmRevision": "", + }], + "components": { + "lib.a.sub.fold": { + "hasOwnPreload": true, + "library": "lib.a" + } + }, + }, oOptions); +}); + +test.serial("integration: Library without dependencies and embeddedBy undefined", async (t) => { + const {verboseLogStub} = t.context; + const workspace = createWorkspace(); + + await createDotLibrary(workspace, resourceFactory, ["test", "lib"]); + + // dependencies + const dependencies = createDependencies({virBasePath: "/"}); + + // lib.a + const embeds = ["sub/fold"]; + await createResources(dependencies, resourceFactory, ["lib", "a"], [], embeds); + // sub + await createManifestResource(dependencies, resourceFactory, ["lib", "a", "sub", "fold"], [], + undefined, undefined); + + const oOptions = { + options: { + projectName: "Test Lib", + pattern: "/resources/**/.library", + rootProject: { + metadata: { + name: "myname" + }, + version: "1.33.7" + } + }, + workspace, + dependencies + }; + await assertCreatedVersionInfo(t, { + "name": "myname", + "scmRevision": "", + "version": "1.33.7", + "libraries": [{ + "name": "lib.a", + "scmRevision": "", + }], + "components": { + "lib.a.sub.fold": { + "library": "lib.a" + } + }, + }, oOptions); + + t.is(verboseLogStub.callCount, 1); + t.is(verboseLogStub.firstCall.args[0], + " component doesn't declare 'sap.app/embeddedBy', don't list it as 'embedded'"); +}); + +test.serial("integration: Library without dependencies and embeddedBy not a string", async (t) => { + const {errorLogStub} = t.context; + const workspace = createWorkspace(); + + await createDotLibrary(workspace, resourceFactory, ["test", "lib"]); + + // dependencies + const dependencies = createDependencies({virBasePath: "/"}); + + // lib.a + const embeds = ["sub/fold"]; + await createResources(dependencies, resourceFactory, ["lib", "a"], [], embeds); + // sub + await createManifestResource(dependencies, resourceFactory, ["lib", "a", "sub", "fold"], [], + undefined, {}); + + const oOptions = { + options: { + projectName: "Test Lib", + pattern: "/resources/**/.library", + rootProject: { + metadata: { + name: "myname" + }, + version: "1.33.7" + } + }, + workspace, + dependencies + }; + await assertCreatedVersionInfo(t, { + "name": "myname", + "scmRevision": "", + "version": "1.33.7", + "libraries": [{ + "name": "lib.a", + "scmRevision": "", + }], + "components": { + "lib.a.sub.fold": { + "library": "lib.a" + } + }, + }, oOptions); + + t.is(errorLogStub.callCount, 1); + t.is(errorLogStub.firstCall.args[0], + " component '%s': property 'sap.app/embeddedBy' is of type '%s' (expected 'string'), " + + "it won't be listed as 'embedded'"); +}); + +test.serial("integration: Library without dependencies and embeddedBy empty string", async (t) => { + const {errorLogStub} = t.context; + const workspace = createWorkspace(); + + await createDotLibrary(workspace, resourceFactory, ["test", "lib"]); + + // dependencies + const dependencies = createDependencies({virBasePath: "/"}); + + // lib.a + const embeds = ["sub/fold"]; + await createResources(dependencies, resourceFactory, ["lib", "a"], [], embeds); + // sub + await createManifestResource(dependencies, resourceFactory, ["lib", "a", "sub", "fold"], [], + undefined, ""); + + const oOptions = { + options: { + projectName: "Test Lib", + pattern: "/resources/**/.library", + rootProject: { + metadata: { + name: "myname" + }, + version: "1.33.7" + } + }, + workspace, + dependencies + }; + await assertCreatedVersionInfo(t, { + "name": "myname", + "scmRevision": "", + "version": "1.33.7", + "libraries": [{ + "name": "lib.a", + "scmRevision": "", + }], + "components": { + "lib.a.sub.fold": { + "library": "lib.a" + } + }, + }, oOptions); + + t.is(errorLogStub.callCount, 1); + t.is(errorLogStub.firstCall.args[0], + " component '%s': property 'sap.app/embeddedBy' has an empty string value (which is invalid), " + + "it won't be listed as 'embedded'"); +}); + +test.serial("integration: Library without dependencies and embeddedBy path not correct", async (t) => { + const {verboseLogStub} = t.context; + const workspace = createWorkspace(); + + await createDotLibrary(workspace, resourceFactory, ["test", "lib"]); + + // dependencies + const dependencies = createDependencies({virBasePath: "/"}); + + // lib.a + const embeds = ["sub/fold"]; + await createResources(dependencies, resourceFactory, ["lib", "a"], [], embeds); + // sub + await createManifestResource(dependencies, resourceFactory, ["lib", "a", "sub", "fold"], [], + undefined, "../"); + + const oOptions = { + options: { + projectName: "Test Lib", + pattern: "/resources/**/.library", + rootProject: { + metadata: { + name: "myname" + }, + version: "1.33.7" + } + }, + workspace, + dependencies + }; + await assertCreatedVersionInfo(t, { + "name": "myname", + "scmRevision": "", + "version": "1.33.7", + "libraries": [{ + "name": "lib.a", + "scmRevision": "", + }], + "components": { + "lib.a.sub.fold": { + "library": "lib.a" + } + }, + }, oOptions); + + t.is(verboseLogStub.callCount, 1); + t.is(verboseLogStub.firstCall.args[0], + " component's 'sap.app/embeddedBy' points to '%s', don't list it as 'embedded'"); +}); + +test.serial("integration: Library with manifest with invalid dependency", async (t) => { + const {infoLogStub} = t.context; + const workspace = createWorkspace(); + + await createDotLibrary(workspace, resourceFactory, ["test", "lib"]); + + // dependencies + const dependencies = createDependencies({virBasePath: "/"}); + + // lib.a + await createResources(dependencies, resourceFactory, ["lib", "a"], [{name: "non.existing"}]); + + + const oOptions = { + options: { + projectName: "Test Lib", + pattern: "/resources/**/.library", + rootProject: { + metadata: { + name: "myname" + }, + version: "1.33.7" + } + }, + workspace, + dependencies + }; + await assertCreatedVersionInfo(t, { + "name": "myname", + "scmRevision": "", + "version": "1.33.7", + "libraries": [{ + "name": "lib.a", + "scmRevision": "", + "manifestHints": { + "dependencies": { + "libs": { + "non.existing": {}, + }, + }, + } + }], + }, oOptions); + + t.is(infoLogStub.callCount, 1); + t.is(infoLogStub.firstCall.args[0], + "Cannot find dependency 'non.existing' defined in the manifest.json or .library file of project 'lib.a'. " + + "This might prevent some UI5 runtime performance optimizations from taking effect. " + + "Please double check your project's dependency configuration."); +});