diff --git a/lib/graph/helpers/ui5Framework.js b/lib/graph/helpers/ui5Framework.js
index 9fe94d1bb..6df9258b0 100644
--- a/lib/graph/helpers/ui5Framework.js
+++ b/lib/graph/helpers/ui5Framework.js
@@ -208,6 +208,60 @@ const utils = {
);
}
},
+ /**
+ * This logic needs to stay in sync with the dependency definitions for the
+ * sapui5/distribution-metadata package.
+ *
+ * @param {@ui5/project/specifications/Project} project
+ */
+ async getFrameworkLibraryDependencies(project) {
+ let dependencies = [];
+ let optionalDependencies = [];
+
+ if (project.getId().startsWith("@sapui5/")) {
+ project.getFrameworkDependencies().forEach((dependency) => {
+ if (dependency.optional) {
+ // Add optional dependency to optionalDependencies
+ optionalDependencies.push(dependency.name);
+ } else if (!dependency.development) {
+ // Add non-development dependency to dependencies
+ dependencies.push(dependency.name);
+ }
+ });
+ } else if (project.getId().startsWith("@openui5/")) {
+ const packageResource = await project.getRootReader().byPath("/package.json");
+ const packageInfo = JSON.parse(await packageResource.getString());
+
+ dependencies = Object.keys(
+ packageInfo.dependencies || {}
+ ).map(($) => $.replace("@openui5/", "")); // @sapui5 dependencies must not be defined in package.json
+ optionalDependencies = Object.keys(
+ packageInfo.devDependencies || {}
+ ).map(($) => $.replace("@openui5/", "")); // @sapui5 dependencies must not be defined in package.json
+ }
+
+ return {dependencies, optionalDependencies};
+ },
+ async getWorkspaceFrameworkLibraryMetadata({workspace, projectGraph}) {
+ const libraryMetadata = Object.create(null);
+ const ui5Modules = await workspace.getModules();
+ for (const ui5Module of ui5Modules) {
+ const {project} = await ui5Module.getSpecifications();
+ // Only framework projects that are not already part of the projectGraph should be handled.
+ // Otherwise they would be available twice which is checked
+ // after installing via checkForDuplicateFrameworkProjects
+ if (project?.isFrameworkProject?.() && !projectGraph.getProject(project.getName())) {
+ const metadata = libraryMetadata[project.getName()] = Object.create(null);
+ metadata.id = project.getId();
+ metadata.path = project.getRootPath();
+ metadata.version = project.getVersion();
+ const {dependencies, optionalDependencies} = await utils.getFrameworkLibraryDependencies(project);
+ metadata.dependencies = dependencies;
+ metadata.optionalDependencies = optionalDependencies;
+ }
+ }
+ return libraryMetadata;
+ },
ProjectProcessor
};
@@ -232,26 +286,31 @@ export default {
* Promise resolving with the given graph instance to allow method chaining
*/
enrichProjectGraph: async function(projectGraph, options = {}) {
+ const {workspace} = options;
const rootProject = projectGraph.getRoot();
const frameworkName = rootProject.getFrameworkName();
const frameworkVersion = rootProject.getFrameworkVersion();
- // It is allowed omit the framework version in ui5.yaml and only provide one via the override
+ // It is allowed to omit the framework version in ui5.yaml and only provide one via the override
// This is a common use case for framework libraries, which generally should not define a
// framework version in their respective ui5.yaml
let version = options.versionOverride || frameworkVersion;
if (rootProject.isFrameworkProject() && !version) {
// If the root project is a framework project and no framework version is defined,
- // all framework dependencies must already be part of the graph
- rootProject.getFrameworkDependencies().forEach((dep) => {
- if (utils.shouldIncludeDependency(dep) && !projectGraph.getProject(dep.name)) {
- throw new Error(
- `Missing framework dependency ${dep.name} for framework project ${rootProject.getName()}`);
- }
+ // all framework dependencies must either be already part of the graph or part of the workspace.
+ // A mixed setup of framework deps within the graph AND from the workspace is currently not supported.
+
+ const someDependencyMissing = rootProject.getFrameworkDependencies().some((dep) => {
+ return utils.shouldIncludeDependency(dep) && !projectGraph.getProject(dep.name);
});
- // All framework dependencies are already present in the graph
- return projectGraph;
+
+ // If all dependencies are available there is nothing else to do here.
+ // In case of a workspace setup, the resolver will be created below without a version and
+ // will succeed in case no library needs to be actually installed.
+ if (!someDependencyMissing) {
+ return projectGraph;
+ }
}
@@ -267,12 +326,6 @@ export default {
);
}
- if (!version) {
- throw new Error(
- `No framework version defined for root project ${rootProject.getName()}`
- );
- }
-
let Resolver;
if (frameworkName === "OpenUI5") {
Resolver = (await import("../../ui5Framework/Openui5Resolver.js")).default;
@@ -296,9 +349,20 @@ export default {
return projectGraph;
}
- log.info(`Using ${frameworkName} version: ${version}`);
+ if (version) {
+ log.info(`Using ${frameworkName} version: ${version}`);
+ }
+
+ let providedLibraryMetadata;
+ if (workspace) {
+ providedLibraryMetadata = await utils.getWorkspaceFrameworkLibraryMetadata({
+ workspace, projectGraph
+ });
+ }
- const resolver = new Resolver({cwd: rootProject.getRootPath(), version});
+ // Note: version might be undefined here and the Resolver will throw an error when calling
+ // #install and it can't be resolved via the provided library metadata
+ const resolver = new Resolver({cwd: rootProject.getRootPath(), version, providedLibraryMetadata});
let startTime;
if (log.isLevelEnabled("verbose")) {
@@ -322,7 +386,7 @@ export default {
const projectProcessor = new utils.ProjectProcessor({
libraryMetadata,
graph: frameworkGraph,
- workspace: options.workspace
+ workspace
});
await Promise.all(referencedLibraries.map(async (libName) => {
diff --git a/lib/ui5Framework/AbstractResolver.js b/lib/ui5Framework/AbstractResolver.js
index e6e9537be..09b281e35 100644
--- a/lib/ui5Framework/AbstractResolver.js
+++ b/lib/ui5Framework/AbstractResolver.js
@@ -27,22 +27,25 @@ const VERSION_RANGE_REGEXP = /^(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
* @hideconstructor
*/
class AbstractResolver {
+ /* eslint-disable max-len */
/**
* @param {*} options options
- * @param {string} options.version Framework version to use
+ * @param {string} [options.version] Framework version to use. When omitted, all libraries need to be available
+ * via providedLibraryMetadata
parameter. Otherwise an error is thrown.
* @param {string} [options.cwd=process.cwd()] Working directory to resolve configurations like .npmrc
* @param {string} [options.ui5HomeDir="~/.ui5"] UI5 home directory location. This will be used to store packages,
* metadata and configuration used by the resolvers. Relative to `process.cwd()`
+ * @param {object.} [options.providedLibraryMetadata]
+ * Resolver skips installing listed libraries and uses the dependency information to resolve their dependencies.
+ * version
can be omitted in case all libraries can be resolved via the providedLibraryMetadata
.
+ * Otherwise an error is thrown.
*/
- constructor({cwd, version, ui5HomeDir}) {
+ /* eslint-enable max-len */
+ constructor({cwd, version, ui5HomeDir, providedLibraryMetadata}) {
if (new.target === AbstractResolver) {
throw new TypeError("Class 'AbstractResolver' is abstract");
}
- if (!version) {
- throw new Error(`AbstractResolver: Missing parameter "version"`);
- }
-
// In some CI environments, the homedir might be set explicitly to a relative
// path (e.g. "./"), but tooling requires an absolute path
this._ui5HomeDir = path.resolve(
@@ -50,6 +53,7 @@ class AbstractResolver {
);
this._cwd = cwd ? path.resolve(cwd) : process.cwd();
this._version = version;
+ this._providedLibraryMetadata = providedLibraryMetadata;
}
async _processLibrary(libraryName, libraryMetadata, errors) {
@@ -62,7 +66,23 @@ class AbstractResolver {
log.verbose("Processing " + libraryName);
- const promises = await this.handleLibrary(libraryName);
+ let promises;
+ const providedLibraryMetadata = this._providedLibraryMetadata?.[libraryName];
+ if (providedLibraryMetadata) {
+ log.verbose(`Skipping install for ${libraryName} (provided)`);
+ promises = {
+ // Use existing metadata if library is provided from outside (e.g. workspace)
+ metadata: Promise.resolve(providedLibraryMetadata),
+ // Provided libraries are already "installed"
+ install: Promise.resolve({
+ pkgPath: providedLibraryMetadata.path
+ })
+ };
+ } else if (!this._version) {
+ throw new Error(`Unable to install library ${libraryName}. No framework version provided.`);
+ } else {
+ promises = await this.handleLibrary(libraryName);
+ }
const [metadata, {pkgPath}] = await Promise.all([
promises.metadata.then((metadata) =>
@@ -143,7 +163,6 @@ class AbstractResolver {
* Object containing all installed libraries with library name as key
*/
- /* eslint-enable max-len */
/**
* Installs the provided libraries and their dependencies
*
diff --git a/test/lib/graph/helpers/ui5Framework.integration.js b/test/lib/graph/helpers/ui5Framework.integration.js
index 132c58461..7cff9788a 100644
--- a/test/lib/graph/helpers/ui5Framework.integration.js
+++ b/test/lib/graph/helpers/ui5Framework.integration.js
@@ -135,7 +135,8 @@ test.afterEach.always((t) => {
function defineTest(testName, {
frameworkName,
- verbose = false
+ verbose = false,
+ librariesInWorkspace = null
}) {
const npmScope = frameworkName === "SAPUI5" ? "@sapui5" : "@openui5";
@@ -410,7 +411,66 @@ function defineTest(testName, {
const provider = new DependencyTreeProvider({dependencyTree});
const projectGraph = await projectGraphBuilder(provider);
- await ui5Framework.enrichProjectGraph(projectGraph);
+ if (librariesInWorkspace) {
+ const projectNameMap = new Map();
+ const moduleIdMap = new Map();
+ librariesInWorkspace.forEach((libName) => {
+ const libraryDistMetadata = distributionMetadata.libraries[libName];
+ const module = {
+ getSpecifications: sinon.stub().resolves({
+ project: {
+ getName: sinon.stub().returns(libName),
+ getVersion: sinon.stub().returns("1.76.0-SNAPSHOT"),
+ getRootPath: sinon.stub().returns(path.join(fakeBaseDir, "workspace", libName)),
+ isFrameworkProject: sinon.stub().returns(true),
+ getId: sinon.stub().returns(libraryDistMetadata.npmPackageName),
+ getRootReader: sinon.stub().returns({
+ byPath: sinon.stub().resolves({
+ getString: sinon.stub().resolves(JSON.stringify({dependencies: {}}))
+ })
+ }),
+ getFrameworkDependencies: sinon.stub().callsFake(() => {
+ const deps = [];
+ libraryDistMetadata.dependencies.forEach((dep) => {
+ deps.push({name: dep});
+ });
+ libraryDistMetadata.optionalDependencies.forEach((optDep) => {
+ deps.push({name: optDep, optional: true});
+ });
+ return deps;
+ }),
+ isDeprecated: sinon.stub().returns(false),
+ isSapInternal: sinon.stub().returns(false),
+ getAllowSapInternal: sinon.stub().returns(false),
+ }
+ }),
+ getVersion: sinon.stub().returns("1.76.0-SNAPSHOT"),
+ getPath: sinon.stub().returns(path.join(fakeBaseDir, "workspace", libName)),
+ };
+ projectNameMap.set(libName, module);
+ moduleIdMap.set(libraryDistMetadata.npmPackageName, module);
+ });
+
+ const getModuleByProjectName = sinon.stub().callsFake(
+ async (projectName) => projectNameMap.get(projectName)
+ );
+ const getModules = sinon.stub().callsFake(
+ async () => {
+ const sortedMap = new Map([...moduleIdMap].sort((a, b) => String(a[0]).localeCompare(b[0])));
+ return Array.from(sortedMap.values());
+ }
+ );
+
+ const workspace = {
+ getName: sinon.stub().returns("test"),
+ getModules,
+ getModuleByProjectName
+ };
+
+ await ui5Framework.enrichProjectGraph(projectGraph, {workspace});
+ } else {
+ await ui5Framework.enrichProjectGraph(projectGraph);
+ }
const callbackStub = sinon.stub().resolves();
await projectGraph.traverseDepthFirst(callbackStub);
@@ -465,6 +525,15 @@ defineTest("ui5Framework helper should enhance project graph with UI5 framework
verbose: true
});
+defineTest("ui5Framework helper should not cause install of libraries within workspace", {
+ frameworkName: "SAPUI5",
+ librariesInWorkspace: ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib8"]
+});
+defineTest("ui5Framework helper should not cause install of libraries within workspace", {
+ frameworkName: "OpenUI5",
+ librariesInWorkspace: ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib8"]
+});
+
function defineErrorTest(testName, {
frameworkName,
failExtract = false,
@@ -723,8 +792,8 @@ test.serial("ui5Framework translator should not try to install anything when no
t.is(pacote.manifest.callCount, 0, "No manifest should be requested");
});
-test.serial("ui5Framework translator should throw an error when framework version is not defined", async (t) => {
- const {ui5Framework, projectGraphBuilder} = t.context;
+test.serial("ui5Framework helper shouldn't throw when framework version and libraries are not provided", async (t) => {
+ const {ui5Framework, projectGraphBuilder, logStub} = t.context;
const dependencyTree = {
id: "test-id",
@@ -745,13 +814,28 @@ test.serial("ui5Framework translator should throw an error when framework versio
const provider = new DependencyTreeProvider({dependencyTree});
const projectGraph = await projectGraphBuilder(provider);
- await t.throwsAsync(async () => {
- await ui5Framework.enrichProjectGraph(projectGraph);
- }, {message: `No framework version defined for root project test-project`}, "Correct error message");
+ await ui5Framework.enrichProjectGraph(projectGraph);
+
+ t.is(logStub.verbose.callCount, 5);
+ t.deepEqual(logStub.verbose.getCall(0).args, [
+ "Configuration for module test-id has been supplied directly"
+ ]);
+ t.deepEqual(logStub.verbose.getCall(1).args, [
+ "Module test-id contains project test-project"
+ ]);
+ t.deepEqual(logStub.verbose.getCall(2).args, [
+ "Root project test-project qualified as application project for project graph"
+ ]);
+ t.deepEqual(logStub.verbose.getCall(3).args, [
+ "Project test-project has no framework dependencies"
+ ]);
+ t.deepEqual(logStub.verbose.getCall(4).args, [
+ "No SAPUI5 libraries referenced in project test-project or in any of its dependencies"
+ ]);
});
test.serial(
- "SAPUI5: ui5Framework translator should throw error when using a library that is not part of the dist metadata",
+ "SAPUI5: ui5Framework helper should throw error when using a library that is not part of the dist metadata",
async (t) => {
const {sinon, ui5Framework, Installer, projectGraphBuilder} = t.context;
diff --git a/test/lib/graph/helpers/ui5Framework.js b/test/lib/graph/helpers/ui5Framework.js
index 52426261d..863646fe4 100644
--- a/test/lib/graph/helpers/ui5Framework.js
+++ b/test/lib/graph/helpers/ui5Framework.js
@@ -100,7 +100,8 @@ test.serial("enrichProjectGraph", async (t) => {
t.is(t.context.Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once");
t.deepEqual(t.context.Sapui5ResolverStub.getCall(0).args, [{
cwd: dependencyTree.path,
- version: dependencyTree.configuration.framework.version
+ version: dependencyTree.configuration.framework.version,
+ providedLibraryMetadata: undefined
}], "Sapui5Resolver#constructor should be called with expected args");
t.is(t.context.Sapui5ResolverInstallStub.callCount, 1, "Sapui5Resolver#install should be called once");
@@ -138,6 +139,34 @@ test.serial("enrichProjectGraph", async (t) => {
], "Traversed graph in correct order");
});
+test.serial("enrichProjectGraph: without framework configuration", async (t) => {
+ const {ui5Framework, log} = t.context;
+ const dependencyTree = {
+ id: "application.a",
+ version: "1.2.3",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ }
+ }
+ };
+
+ t.is(log.verbose.callCount, 0);
+
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+
+ await ui5Framework.enrichProjectGraph(projectGraph);
+ t.is(projectGraph.getSize(), 1, "Project graph should remain unchanged");
+ t.is(log.verbose.callCount, 1);
+ t.deepEqual(log.verbose.getCall(0).args, [
+ "Root project application.a has no framework configuration. Nothing to do here"
+ ]);
+});
+
test.serial("enrichProjectGraph: With versionOverride", async (t) => {
const {
sinon, ui5Framework, utils,
@@ -186,12 +215,13 @@ test.serial("enrichProjectGraph: With versionOverride", async (t) => {
t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once");
t.deepEqual(Sapui5ResolverStub.getCall(0).args, [{
cwd: dependencyTree.path,
- version: "1.99.9"
+ version: "1.99.9",
+ providedLibraryMetadata: undefined
}], "Sapui5Resolver#constructor should be called with expected args");
});
-test.serial("enrichProjectGraph should throw error when no framework version is provided", async (t) => {
- const {ui5Framework} = t.context;
+test.serial("enrichProjectGraph shouldn't throw when no framework version and no libraries are provided", async (t) => {
+ const {ui5Framework, log} = t.context;
const dependencyTree = {
id: "test-id",
version: "1.2.3",
@@ -211,9 +241,15 @@ test.serial("enrichProjectGraph should throw error when no framework version is
const provider = new DependencyTreeProvider({dependencyTree});
const projectGraph = await projectGraphBuilder(provider);
- await t.throwsAsync(async () => {
- await ui5Framework.enrichProjectGraph(projectGraph);
- }, {message: "No framework version defined for root project application.a"});
+ await ui5Framework.enrichProjectGraph(projectGraph);
+
+ t.is(log.verbose.callCount, 2);
+ t.deepEqual(log.verbose.getCall(0).args, [
+ "Project application.a has no framework dependencies"
+ ]);
+ t.deepEqual(log.verbose.getCall(1).args, [
+ "No SAPUI5 libraries referenced in project application.a or in any of its dependencies"
+ ]);
});
test.serial("enrichProjectGraph should skip framework project without version", async (t) => {
@@ -314,7 +350,8 @@ test.serial("enrichProjectGraph should resolve framework project with version an
t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once");
t.deepEqual(Sapui5ResolverStub.getCall(0).args, [{
cwd: dependencyTree.path,
- version: "1.2.3"
+ version: "1.2.3",
+ providedLibraryMetadata: undefined
}], "Sapui5Resolver#constructor should be called with expected args");
});
@@ -390,15 +427,56 @@ test.serial("enrichProjectGraph should resolve framework project " +
t.is(projectGraph.getSize(), 2, "Project graph should remain unchanged");
t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once");
- t.is(getFrameworkLibrariesFromGraphStub.callCount, 1, "getFrameworkLibrariesFromGrap should be called once");
+ t.is(getFrameworkLibrariesFromGraphStub.callCount, 1, "getFrameworkLibrariesFromGraph should be called once");
t.deepEqual(Sapui5ResolverStub.getCall(0).args, [{
cwd: dependencyTree.path,
- version: "1.99.9"
+ version: "1.99.9",
+ providedLibraryMetadata: undefined
}], "Sapui5Resolver#constructor should be called with expected args");
});
-test.serial("enrichProjectGraph should throw for framework project with dependency missing in graph", async (t) => {
+test.serial("enrichProjectGraph should skip framework project when all dependencies are in graph", async (t) => {
const {ui5Framework} = t.context;
+ const dependencyTree = {
+ id: "@sapui5/project",
+ version: "1.2.3",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ },
+ framework: {
+ name: "SAPUI5",
+ libraries: [
+ {name: "lib1"}
+ ]
+ }
+ },
+ dependencies: [{
+ id: "@openui5/lib1",
+ version: "1.2.3",
+ path: libraryEPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "library",
+ metadata: {
+ name: "lib1"
+ }
+ }
+ }]
+ };
+
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+
+ await ui5Framework.enrichProjectGraph(projectGraph);
+ t.is(projectGraph.getSize(), 2, "Project graph should remain unchanged");
+});
+
+test.serial("enrichProjectGraph should throw for framework project with dependency missing in graph", async (t) => {
+ const {ui5Framework, Sapui5ResolverInstallStub} = t.context;
const dependencyTree = {
id: "@sapui5/project",
version: "1.2.3",
@@ -420,12 +498,15 @@ test.serial("enrichProjectGraph should throw for framework project with dependen
}
};
+ const installError = new Error("Resolution of framework libraries failed with errors: TEST ERROR");
+
+ Sapui5ResolverInstallStub.rejects(installError);
+
const provider = new DependencyTreeProvider({dependencyTree});
const projectGraph = await projectGraphBuilder(provider);
const err = await t.throwsAsync(ui5Framework.enrichProjectGraph(projectGraph));
- t.is(err.message, `Missing framework dependency lib1 for framework project application.a`,
- "Threw with expected error message");
+ t.is(err.message, installError.message);
});
test.serial("enrichProjectGraph should throw for incorrect framework name", async (t) => {
@@ -561,6 +642,120 @@ test.serial("enrichProjectGraph should throw error when projectGraph contains a
});
});
+test.serial("enrichProjectGraph should use framework library metadata from workspace", async (t) => {
+ const {ui5Framework, utils, Sapui5ResolverStub, Sapui5ResolverInstallStub, sinon} = t.context;
+ const dependencyTree = {
+ id: "@sapui5/project",
+ version: "1.2.3",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ },
+ framework: {
+ name: "SAPUI5",
+ version: "1.111.1",
+ libraries: [
+ {name: "lib1"},
+ {name: "lib2"}
+ ]
+ }
+ }
+ };
+
+ const workspace = {
+ getName: sinon.stub().resolves("default")
+ };
+
+ const workspaceFrameworkLibraryMetadata = {};
+ const libraryMetadata = {};
+
+ sinon.stub(utils, "getWorkspaceFrameworkLibraryMetadata").resolves(workspaceFrameworkLibraryMetadata);
+ Sapui5ResolverInstallStub.resolves({libraryMetadata});
+
+ const addProjectToGraphStub = sinon.stub();
+ sinon.stub(utils, "ProjectProcessor")
+ .callsFake(() => {
+ return {
+ addProjectToGraph: addProjectToGraphStub
+ };
+ });
+
+ sinon.stub(utils, "declareFrameworkDependenciesInGraph").resolves();
+
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider, {workspace});
+
+ await ui5Framework.enrichProjectGraph(projectGraph, {workspace});
+
+ t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once");
+ t.deepEqual(Sapui5ResolverStub.getCall(0).args, [{
+ cwd: dependencyTree.path,
+ version: "1.111.1",
+ providedLibraryMetadata: workspaceFrameworkLibraryMetadata
+ }], "Sapui5Resolver#constructor should be called with expected args");
+ t.is(Sapui5ResolverStub.getCall(0).args[0].providedLibraryMetadata, workspaceFrameworkLibraryMetadata);
+});
+
+test.serial("enrichProjectGraph should allow omitting framework version in case " +
+ "all framework libraries come from the workspace", async (t) => {
+ const {ui5Framework, utils, Sapui5ResolverStub, Sapui5ResolverInstallStub, sinon} = t.context;
+ const dependencyTree = {
+ id: "@sapui5/project",
+ version: "1.2.3",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ },
+ framework: {
+ name: "SAPUI5",
+ libraries: [
+ {name: "lib1"},
+ {name: "lib2"}
+ ]
+ }
+ }
+ };
+
+ const workspace = {
+ getName: sinon.stub().resolves("default")
+ };
+
+ const workspaceFrameworkLibraryMetadata = {};
+ const libraryMetadata = {};
+
+ sinon.stub(utils, "getWorkspaceFrameworkLibraryMetadata").resolves(workspaceFrameworkLibraryMetadata);
+ Sapui5ResolverInstallStub.resolves({libraryMetadata});
+
+ const addProjectToGraphStub = sinon.stub();
+ sinon.stub(utils, "ProjectProcessor")
+ .callsFake(() => {
+ return {
+ addProjectToGraph: addProjectToGraphStub
+ };
+ });
+
+ sinon.stub(utils, "declareFrameworkDependenciesInGraph").resolves();
+
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider, {workspace});
+
+ await ui5Framework.enrichProjectGraph(projectGraph, {workspace});
+
+ t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once");
+ t.deepEqual(Sapui5ResolverStub.getCall(0).args, [{
+ cwd: dependencyTree.path,
+ version: undefined,
+ providedLibraryMetadata: workspaceFrameworkLibraryMetadata
+ }], "Sapui5Resolver#constructor should be called with expected args");
+ t.is(Sapui5ResolverStub.getCall(0).args[0].providedLibraryMetadata, workspaceFrameworkLibraryMetadata);
+});
+
test.serial("utils.shouldIncludeDependency", (t) => {
const {utils} = t.context;
// root project dependency should always be included
@@ -1102,6 +1297,240 @@ test("utils.checkForDuplicateFrameworkProjects: Two duplicates", (t) => {
});
});
+test("utils.getFrameworkLibraryDependencies: OpenUI5 library", async (t) => {
+ const {utils, sinon} = t.context;
+
+ const project = {
+ getId: sinon.stub().returns("@openui5/sap.ui.lib1"),
+ getRootReader: sinon.stub().returns({
+ byPath: sinon.stub().withArgs("/package.json").resolves({
+ getString: sinon.stub().resolves(JSON.stringify({
+ dependencies: {
+ "@openui5/sap.ui.lib2": "*"
+ },
+ devDependencies: {
+ "@openui5/themelib_fancy": "*"
+ }
+ }))
+ })
+ }),
+ };
+
+ const result = await utils.getFrameworkLibraryDependencies(project);
+ t.deepEqual(result, {
+ dependencies: ["sap.ui.lib2"],
+ optionalDependencies: ["themelib_fancy"]
+ });
+});
+
+test("utils.getFrameworkLibraryDependencies: SAPUI5 library", async (t) => {
+ const {utils, sinon} = t.context;
+
+ const project = {
+ getId: sinon.stub().returns("@sapui5/sap.ui.lib1"),
+ getFrameworkDependencies: sinon.stub().returns([
+ {
+ name: "sap.ui.lib2"
+ },
+ {
+ name: "themelib_fancy",
+ optional: true
+ },
+ {
+ name: "sap.ui.lib3",
+ development: true
+ }
+ ])
+ };
+
+ const result = await utils.getFrameworkLibraryDependencies(project);
+ t.deepEqual(result, {
+ dependencies: ["sap.ui.lib2"],
+ optionalDependencies: ["themelib_fancy"]
+ });
+});
+
+test("utils.getFrameworkLibraryDependencies: OpenUI5 library - no dependencies", async (t) => {
+ const {utils, sinon} = t.context;
+
+ const project = {
+ getId: sinon.stub().returns("@openui5/sap.ui.lib1"),
+ getRootReader: sinon.stub().returns({
+ byPath: sinon.stub().withArgs("/package.json").resolves({
+ getString: sinon.stub().resolves(JSON.stringify({}))
+ })
+ }),
+ };
+
+ const result = await utils.getFrameworkLibraryDependencies(project);
+ t.deepEqual(result, {
+ dependencies: [],
+ optionalDependencies: []
+ });
+});
+
+test("utils.getFrameworkLibraryDependencies: No framework library", async (t) => {
+ const {utils, sinon} = t.context;
+
+ const project = {
+ getId: sinon.stub().returns("foo")
+ };
+
+ const result = await utils.getFrameworkLibraryDependencies(project);
+ t.deepEqual(result, {
+ dependencies: [],
+ optionalDependencies: []
+ });
+});
+
+test("utils.getWorkspaceFrameworkLibraryMetadata: No workspace modules", async (t) => {
+ const {utils, sinon} = t.context;
+
+ const workspace = {
+ getModules: sinon.stub().resolves([])
+ };
+
+ const libraryMetadata = await utils.getWorkspaceFrameworkLibraryMetadata({workspace, projectGraph: {}});
+
+ t.deepEqual(libraryMetadata, {});
+});
+
+test("utils.getWorkspaceFrameworkLibraryMetadata: With workspace modules", async (t) => {
+ const {utils, sinon} = t.context;
+
+ const workspace = {
+ getModules: sinon.stub().resolves([
+ {
+ // Extensions don't have projects, should be ignored
+ getSpecifications: sinon.stub().resolves({
+ project: null
+ })
+ },
+ {
+ getSpecifications: sinon.stub().resolves({
+ project: {
+ // some types don't have a "isFrameworkProject" method
+ }
+ })
+ },
+ {
+ getSpecifications: sinon.stub().resolves({
+ project: {
+ isFrameworkProject: sinon.stub().returns(false)
+ }
+ })
+ },
+ {
+ getSpecifications: sinon.stub().resolves({
+ project: {
+ isFrameworkProject: sinon.stub().returns(true),
+ getName: sinon.stub().returns("sap.ui.lib1"),
+ getId: sinon.stub().returns("@openui5/sap.ui.lib1"),
+ getRootPath: sinon.stub().returns("/rootPath"),
+ getRootReader: sinon.stub().returns({
+ byPath: sinon.stub().withArgs("/package.json").resolves({
+ getString: sinon.stub().resolves(JSON.stringify({
+ dependencies: {
+ "@openui5/sap.ui.lib2": "*"
+ },
+ devDependencies: {
+ "@openui5/themelib_fancy": "*"
+ }
+ }))
+ })
+ }),
+ getVersion: sinon.stub().returns("1.0.0"),
+ }
+ })
+ },
+ {
+ getSpecifications: sinon.stub().resolves({
+ project: {
+ isFrameworkProject: sinon.stub().returns(true),
+ getName: sinon.stub().returns("sap.ui.lib3"),
+ getId: sinon.stub().returns("@sapui5/sap.ui.lib3"),
+ getRootPath: sinon.stub().returns("/rootPath"),
+ getVersion: sinon.stub().returns("1.0.0"),
+ getFrameworkDependencies: sinon.stub().returns([
+ {
+ name: "sap.ui.lib4"
+ },
+ {
+ name: "sap.ui.lib5",
+ optional: true
+ },
+ {
+ name: "sap.ui.lib6",
+ development: true
+ }
+ ])
+ }
+ })
+ }
+ ])
+ };
+
+ const getProject = sinon.stub();
+ getProject.withArgs("sap.ui.lib1").returns(undefined);
+ const projectGraph = {
+ getProject
+ };
+
+ const libraryMetadata = await utils.getWorkspaceFrameworkLibraryMetadata({workspace, projectGraph});
+
+ t.deepEqual(libraryMetadata, {
+ "sap.ui.lib1": {
+ dependencies: [
+ "sap.ui.lib2"
+ ],
+ id: "@openui5/sap.ui.lib1",
+ optionalDependencies: [
+ "themelib_fancy"
+ ],
+ path: "/rootPath",
+ version: "1.0.0"
+ },
+ "sap.ui.lib3": {
+ dependencies: [
+ "sap.ui.lib4"
+ ],
+ id: "@sapui5/sap.ui.lib3",
+ optionalDependencies: [
+ "sap.ui.lib5"
+ ],
+ path: "/rootPath",
+ version: "1.0.0"
+ },
+ });
+});
+
+test("utils.getWorkspaceFrameworkLibraryMetadata: With workspace module within projectGraph", async (t) => {
+ const {utils, sinon} = t.context;
+
+ const workspace = {
+ getModules: sinon.stub().resolves([
+ {
+ getSpecifications: sinon.stub().resolves({
+ project: {
+ isFrameworkProject: sinon.stub().returns(true),
+ getName: sinon.stub().returns("sap.ui.lib1")
+ }
+ })
+ }
+ ])
+ };
+
+ const getProject = sinon.stub();
+ getProject.withArgs("sap.ui.lib1").returns({});
+ const projectGraph = {
+ getProject
+ };
+
+ const libraryMetadata = await utils.getWorkspaceFrameworkLibraryMetadata({workspace, projectGraph});
+
+ t.deepEqual(libraryMetadata, {});
+});
+
test.serial("ProjectProcessor: Add project to graph", async (t) => {
const {sinon} = t.context;
const {ProjectProcessor} = t.context.utils;
diff --git a/test/lib/ui5framework/AbstractResolver.js b/test/lib/ui5framework/AbstractResolver.js
index 7b1da9df3..a89d48ed0 100644
--- a/test/lib/ui5framework/AbstractResolver.js
+++ b/test/lib/ui5framework/AbstractResolver.js
@@ -38,19 +38,24 @@ test("AbstractResolver: abstract constructor should throw", async (t) => {
test("AbstractResolver: constructor", (t) => {
const {MyResolver, AbstractResolver} = t.context;
+ const providedLibraryMetadata = {"test": "data"};
const resolver = new MyResolver({
cwd: "/test-project/",
- version: "1.75.0"
+ version: "1.75.0",
+ providedLibraryMetadata
});
t.true(resolver instanceof MyResolver, "Constructor returns instance of sub-class");
t.true(resolver instanceof AbstractResolver, "Constructor returns instance of abstract class");
+ t.is(resolver._version, "1.75.0");
+ t.is(resolver._providedLibraryMetadata, providedLibraryMetadata);
});
-test("AbstractResolver: constructor requires 'version'", (t) => {
+test("AbstractResolver: constructor without version", (t) => {
const {MyResolver} = t.context;
- t.throws(() => {
- new MyResolver({});
- }, {message: `AbstractResolver: Missing parameter "version"`});
+ const resolver = new MyResolver({
+ cwd: "/test-project/"
+ });
+ t.is(resolver._version, undefined);
});
test("AbstractResolver: Set absolute 'cwd'", (t) => {
@@ -204,9 +209,160 @@ test("AbstractResolver: install", async (t) => {
install: Promise.resolve({pkgPath: "/foo/sap.ui.lib4"})
});
- await resolver.install(["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib4"]);
+ const result = await resolver.install(["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib4"]);
t.is(handleLibraryStub.callCount, 4, "Each library should be handled once");
+ t.deepEqual(result, {
+ libraryMetadata: {
+ "sap.ui.lib1": {
+ dependencies: [],
+ npmPackageName: "@openui5/sap.ui.lib1",
+ optionalDependencies: [],
+ path: "/foo/sap.ui.lib1",
+ version: "1.75.0",
+ },
+ "sap.ui.lib2": {
+ dependencies: [
+ "sap.ui.lib3",
+ ],
+ npmPackageName: "@openui5/sap.ui.lib2",
+ optionalDependencies: [],
+ path: "/foo/sap.ui.lib2",
+ version: "1.75.0",
+ },
+ "sap.ui.lib3": {
+ dependencies: [],
+ npmPackageName: "@openui5/sap.ui.lib3",
+ optionalDependencies: [
+ "sap.ui.lib4",
+ ],
+ path: "/foo/sap.ui.lib3",
+ version: "1.75.0",
+ },
+ "sap.ui.lib4": {
+ dependencies: [
+ "sap.ui.lib1",
+ ],
+ npmPackageName: "@openui5/sap.ui.lib4",
+ optionalDependencies: [],
+ path: "/foo/sap.ui.lib4",
+ version: "1.75.0",
+ },
+ }
+ });
+});
+
+test("AbstractResolver: install (with providedLibraryMetadata)", async (t) => {
+ const {MyResolver} = t.context;
+ const resolver = new MyResolver({
+ cwd: "/test-project/",
+ version: "1.75.0",
+ providedLibraryMetadata: {
+ "sap.ui.lib1": {
+ "npmPackageName": "@openui5/sap.ui.lib1",
+ "version": "1.75.0-workspace",
+ "dependencies": [
+ "sap.ui.lib3"
+ ],
+ "optionalDependencies": [],
+ "path": "/workspace/sap.ui.lib1"
+ },
+ "sap.ui.lib4": {
+ "npmPackageName": "@openui5/sap.ui.lib4",
+ "version": "1.75.0-workspace",
+ "dependencies": [
+ "sap.ui.lib5"
+ ],
+ "optionalDependencies": [],
+ "path": "/workspace/sap.ui.lib4"
+ },
+ "sap.ui.lib5": {
+ "npmPackageName": "@openui5/sap.ui.lib5",
+ "version": "1.75.0-workspace",
+ "dependencies": [],
+ "optionalDependencies": [],
+ "path": "/workspace/sap.ui.lib5"
+ },
+ }
+ });
+
+ const metadata = {
+ libraries: {
+ "sap.ui.lib2": {
+ "npmPackageName": "@openui5/sap.ui.lib2",
+ "version": "1.75.0",
+ "dependencies": [],
+ "optionalDependencies": []
+ },
+ "sap.ui.lib3": {
+ "npmPackageName": "@openui5/sap.ui.lib3",
+ "version": "1.75.0",
+ "dependencies": [
+ "sap.ui.lib4"
+ ],
+ "optionalDependencies": []
+ },
+ }
+ };
+
+ const handleLibraryStub = sinon.stub(resolver, "handleLibrary");
+ handleLibraryStub
+ .callsFake(async (libraryName) => {
+ throw new Error(`Unknown handleLibrary call: ${libraryName}`);
+ })
+ .withArgs("sap.ui.lib2").resolves({
+ metadata: Promise.resolve(metadata.libraries["sap.ui.lib2"]),
+ install: Promise.resolve({pkgPath: "/foo/sap.ui.lib2"})
+ })
+ .withArgs("sap.ui.lib3").resolves({
+ metadata: Promise.resolve(metadata.libraries["sap.ui.lib3"]),
+ install: Promise.resolve({pkgPath: "/foo/sap.ui.lib3"})
+ });
+
+ const result = await resolver.install(["sap.ui.lib1", "sap.ui.lib2"]);
+
+ t.is(handleLibraryStub.callCount, 2, "Each library not part of providedLibraryMetadata should be handled once");
+ t.deepEqual(result, {
+ libraryMetadata: {
+ "sap.ui.lib1": {
+ dependencies: ["sap.ui.lib3"],
+ npmPackageName: "@openui5/sap.ui.lib1",
+ optionalDependencies: [],
+ path: "/workspace/sap.ui.lib1",
+ version: "1.75.0-workspace",
+ },
+ "sap.ui.lib2": {
+ dependencies: [],
+ npmPackageName: "@openui5/sap.ui.lib2",
+ optionalDependencies: [],
+ path: "/foo/sap.ui.lib2",
+ version: "1.75.0",
+ },
+ "sap.ui.lib3": {
+ dependencies: ["sap.ui.lib4",],
+ npmPackageName: "@openui5/sap.ui.lib3",
+ optionalDependencies: [],
+ path: "/foo/sap.ui.lib3",
+ version: "1.75.0",
+ },
+ "sap.ui.lib4": {
+ dependencies: [
+ "sap.ui.lib5",
+ ],
+ npmPackageName: "@openui5/sap.ui.lib4",
+ optionalDependencies: [],
+ path: "/workspace/sap.ui.lib4",
+ version: "1.75.0-workspace",
+ },
+ "sap.ui.lib5": {
+ dependencies: [],
+ npmPackageName: "@openui5/sap.ui.lib5",
+ optionalDependencies: [],
+ path: "/workspace/sap.ui.lib5",
+ version: "1.75.0-workspace",
+ },
+ }
+ });
});
test("AbstractResolver: install error handling (rejection of metadata/install)", async (t) => {
@@ -335,6 +491,49 @@ Failed to resolve library sap.ui.lib2: Error within handleLibrary: sap.ui.lib2`}
t.is(handleLibraryStub.callCount, 2, "Each library should be handled once");
});
+test("AbstractResolver: install error handling " +
+"(no version, no providedLibraryMetadata)", async (t) => {
+ const {MyResolver} = t.context;
+ const resolver = new MyResolver({
+ cwd: "/test-project/",
+ });
+
+ const handleLibraryStub = sinon.stub(resolver, "handleLibrary");
+
+ await t.throwsAsync(resolver.install(["sap.ui.lib1", "sap.ui.lib2"]), {
+ message: `Resolution of framework libraries failed with errors:
+Failed to resolve library sap.ui.lib1: Unable to install library sap.ui.lib1. No framework version provided.
+Failed to resolve library sap.ui.lib2: Unable to install library sap.ui.lib2. No framework version provided.`
+ });
+
+ t.is(handleLibraryStub.callCount, 0, "Handle library should not be called when no version is available");
+});
+
+test("AbstractResolver: install error handling " +
+"(no version, one lib not part of providedLibraryMetadata)", async (t) => {
+ const {MyResolver} = t.context;
+ const resolver = new MyResolver({
+ cwd: "/test-project/",
+ providedLibraryMetadata: {
+ "sap.ui.lib1": {
+ "npmPackageName": "@openui5/sap.ui.lib1",
+ "version": "1.75.0-SNAPSHOT",
+ "dependencies": [],
+ "optionalDependencies": []
+ }
+ }
+ });
+
+ const handleLibraryStub = sinon.stub(resolver, "handleLibrary");
+
+ await t.throwsAsync(resolver.install(["sap.ui.lib1", "sap.ui.lib2"]), {
+ message: `Resolution of framework libraries failed with errors:
+Failed to resolve library sap.ui.lib2: Unable to install library sap.ui.lib2. No framework version provided.`
+ });
+
+ t.is(handleLibraryStub.callCount, 0, "Handle library should not be called when no version is available");
+});
+
test("AbstractResolver: static fetchAllVersions should throw an Error when not implemented", async (t) => {
const {AbstractResolver} = t.context;
await t.throwsAsync(async () => {