Skip to content

Commit

Permalink
[INTERNAL] Allow framework projects use of framework resolver (#561)
Browse files Browse the repository at this point in the history
If the current root project is a framework project (identified by the scope in the package.json name), allow processing of it's framework dependencies by the ui5Framework helper module. Other framework projects in the graph are still ignored.

Framework dependencies of non-framework projects are always processed. Framework dependencies of framework projects have always been ignored. With this change, framework dependencies of the root project are always processed, no matter whether it's a framework projects or not.

This allows framework developers to retrieve all required dependencies in a given version. The version is either defined in the ui5.yaml (not recommended) or via the --framework-version CLI parameter.

If no version is specified, all required framework dependencies must be part of the presented graph or an error is thrown.
  • Loading branch information
RandomByte authored Jan 30, 2023
1 parent 6307659 commit f90d17a
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 37 deletions.
41 changes: 24 additions & 17 deletions lib/graph/helpers/ui5Framework.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const utils = {
const ui5Dependencies = [];
const rootProject = projectGraph.getRoot();
await projectGraph.traverseBreadthFirst(async ({project}) => {
if (project.isFrameworkProject()) {
if (project !== rootProject && project.isFrameworkProject()) {
// Ignoring UI5 Framework libraries in dependencies
return;
}
Expand All @@ -93,7 +93,7 @@ const utils = {
async declareFrameworkDependenciesInGraph(projectGraph) {
const rootProject = projectGraph.getRoot();
await projectGraph.traverseBreadthFirst(async ({project}) => {
if (project.isFrameworkProject()) {
if (project !== rootProject && project.isFrameworkProject()) {
// Ignoring UI5 Framework libraries in dependencies
return;
}
Expand Down Expand Up @@ -147,7 +147,7 @@ const utils = {
*
*
* @private
* @module @ui5/project/translators/ui5Framework
* @module @ui5/project/helpers/ui5Framework
*/
export default {
/**
Expand All @@ -157,26 +157,34 @@ export default {
* @param {@ui5/project/graph/ProjectGraph} projectGraph
* @param {object} [options]
* @param {string} [options.versionOverride] Framework version to use instead of the root projects framework
* version from the provided <code>tree</code>
* version
* @returns {Promise<@ui5/project/graph/ProjectGraph>}
* Promise resolving with the given graph instance to allow method chaining
*/
enrichProjectGraph: async function(projectGraph, options = {}) {
const rootProject = projectGraph.getRoot();
const frameworkName = rootProject.getFrameworkName();
const frameworkVersion = rootProject.getFrameworkVersion();

if (rootProject.isFrameworkProject()) {
// It is allowed 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 project ${rootProject.getName()}`);
`Missing framework dependency ${dep.name} for framework project ${rootProject.getName()}`);
}
});
// Ignoring UI5 Framework libraries in dependencies
// All framework dependencies are already present in the graph
return projectGraph;
}

const frameworkName = rootProject.getFrameworkName();
const frameworkVersion = rootProject.getFrameworkVersion();

if (!frameworkName && !frameworkVersion) {
log.verbose(`Root project ${rootProject.getName()} has no framework configuration. Nothing to do here`);
return projectGraph;
Expand All @@ -189,26 +197,25 @@ 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;
} else if (frameworkName === "SAPUI5") {
Resolver = (await import("../../ui5Framework/Sapui5Resolver.js")).default;
}

let version;
if (!frameworkVersion) {
throw new Error(
`No framework version defined for root project ${rootProject.getName()}`
);
} else if (options.versionOverride) {
if (options.versionOverride) {
version = await Resolver.resolveVersion(options.versionOverride, {cwd: rootProject.getRootPath()});
log.info(
`Overriding configured ${frameworkName} version ` +
`${frameworkVersion} with version ${version}`
);
} else {
version = frameworkVersion;
}

const referencedLibraries = await utils.getFrameworkLibrariesFromGraph(projectGraph);
Expand Down
159 changes: 139 additions & 20 deletions test/lib/graph/helpers/ui5Framework.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ test.serial("ui5Framework translator should throw an error when framework versio
], "Traversed graph in correct order");
});

test.serial("generateDependencyTree (with versionOverride)", async (t) => {
test.serial("enrichProjectGraph (with versionOverride)", async (t) => {
const {
sinon, ui5Framework, utils,
Sapui5ResolverStub, Sapui5ResolverResolveVersionStub, Sapui5ResolverInstallStub
Expand Down Expand Up @@ -179,7 +179,7 @@ test.serial("generateDependencyTree (with versionOverride)", async (t) => {
}], "Sapui5Resolver#constructor should be called with expected args");
});

test.serial("generateDependencyTree should throw error when no framework version is provided", async (t) => {
test.serial("enrichProjectGraph should throw error when no framework version is provided", async (t) => {
const {ui5Framework} = t.context;
const dependencyTree = {
id: "test-id",
Expand All @@ -203,15 +203,9 @@ test.serial("generateDependencyTree should throw error when no framework version
await t.throwsAsync(async () => {
await ui5Framework.enrichProjectGraph(projectGraph);
}, {message: "No framework version defined for root project application.a"});

await t.throwsAsync(async () => {
await ui5Framework.enrichProjectGraph(projectGraph, {
versionOverride: "1.75.0"
});
}, {message: "No framework version defined for root project application.a"});
});

test.serial("generateDependencyTree should skip framework project without version", async (t) => {
test.serial("enrichProjectGraph should skip framework project without version", async (t) => {
const {ui5Framework} = t.context;
const dependencyTree = {
id: "@sapui5/project",
Expand All @@ -235,8 +229,14 @@ test.serial("generateDependencyTree should skip framework project without versio
t.is(projectGraph.getSize(), 1, "Project graph should remain unchanged");
});

test.serial("generateDependencyTree should skip framework project with version and framework config", async (t) => {
const {ui5Framework} = t.context;
test.serial("enrichProjectGraph should resolve framework project with version and framework config", async (t) => {
// Framework projects should not specify framework versions, but they might do so in dedicated configuration files
// In this case the graph is generated the usual way for the root-project. However, framework projects on
// other levels of the graph are ignored
const {
sinon, ui5Framework, utils,
Sapui5ResolverStub, Sapui5ResolverInstallStub
} = t.context;
const dependencyTree = {
id: "@sapui5/project",
version: "1.2.3",
Expand All @@ -257,17 +257,136 @@ test.serial("generateDependencyTree should skip framework project with version a
}
]
}
}
},
dependencies: [{
id: "@openui5/test1", // Will not be scanned
version: "1.2.3",
path: libraryEPath,
configuration: {
specVersion: "2.0",
type: "library",
metadata: {
name: "library.d"
},
framework: {
name: "OpenUI5",
libraries: [{
name: "lib2"
}]
}
}
}]
};
const referencedLibraries = ["lib1"];
const libraryMetadata = {fake: "metadata"};

const getFrameworkLibrariesFromGraphStub =
sinon.stub(utils, "getFrameworkLibrariesFromGraph").resolves(referencedLibraries);

Sapui5ResolverInstallStub.resolves({libraryMetadata});

const addProjectToGraphStub = sinon.stub();
sinon.stub(utils, "ProjectProcessor")
.callsFake(() => {
return {
addProjectToGraph: addProjectToGraphStub
};
});

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(projectGraph.getSize(), 2, "Project graph should remain unchanged");

t.is(getFrameworkLibrariesFromGraphStub.callCount, 1, "getFrameworkLibrariesFromGrap should be called once");
t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once");
t.deepEqual(Sapui5ResolverStub.getCall(0).args, [{
cwd: dependencyTree.path,
version: "1.2.3"
}], "Sapui5Resolver#constructor should be called with expected args");
});

test.serial("enrichProjectGraph should resolve framework project " +
"with framework config and version override", async (t) => {
// Framework projects should not specify framework versions, but they might do so in dedicated configuration files
// In this case the graph is generated the usual way for the root-project. However, framework projects on
// other levels of the graph are ignored
const {
sinon, ui5Framework, utils,
Sapui5ResolverStub, Sapui5ResolverResolveVersionStub, Sapui5ResolverInstallStub
} = 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",
optional: true
}
]
}
},
dependencies: [{
id: "@openui5/test1", // Will not be scanned
version: "1.2.3",
path: libraryEPath,
configuration: {
specVersion: "2.0",
type: "library",
metadata: {
name: "library.d"
},
framework: {
name: "OpenUI5",
libraries: [{
name: "lib2"
}]
}
}
}]
};
const referencedLibraries = ["lib1"];
const libraryMetadata = {fake: "metadata"};

const getFrameworkLibrariesFromGraphStub =
sinon.stub(utils, "getFrameworkLibrariesFromGraph").resolves(referencedLibraries);

Sapui5ResolverInstallStub.resolves({libraryMetadata});
Sapui5ResolverResolveVersionStub.resolves("1.99.9");

const addProjectToGraphStub = sinon.stub();
sinon.stub(utils, "ProjectProcessor")
.callsFake(() => {
return {
addProjectToGraph: addProjectToGraphStub
};
});

const provider = new DependencyTreeProvider({dependencyTree});
const projectGraph = await projectGraphBuilder(provider);

await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride: "3.4.5"});
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.deepEqual(Sapui5ResolverStub.getCall(0).args, [{
cwd: dependencyTree.path,
version: "1.99.9"
}], "Sapui5Resolver#constructor should be called with expected args");
});

test.serial("generateDependencyTree should throw for framework project with dependency missing in graph", async (t) => {
test.serial("enrichProjectGraph should throw for framework project with dependency missing in graph", async (t) => {
const {ui5Framework} = t.context;
const dependencyTree = {
id: "@sapui5/project",
Expand All @@ -281,7 +400,6 @@ test.serial("generateDependencyTree should throw for framework project with depe
},
framework: {
name: "SAPUI5",
version: "1.2.3",
libraries: [
{
name: "lib1"
Expand All @@ -295,11 +413,11 @@ test.serial("generateDependencyTree should throw for framework project with depe
const projectGraph = await projectGraphBuilder(provider);

const err = await t.throwsAsync(ui5Framework.enrichProjectGraph(projectGraph));
t.is(err.message, `Missing framework dependency lib1 for project application.a`,
t.is(err.message, `Missing framework dependency lib1 for framework project application.a`,
"Threw with expected error message");
});

test.serial("generateDependencyTree should ignore root project without framework configuration", async (t) => {
test.serial("enrichProjectGraph should ignore root project without framework configuration", async (t) => {
const {ui5Framework} = t.context;
const dependencyTree = {
id: "@sapui5/project",
Expand Down Expand Up @@ -376,7 +494,8 @@ test.serial("utils.getFrameworkLibrariesFromTree: Project without dependencies",
t.deepEqual(ui5Dependencies, []);
});

test.serial("utils.getFrameworkLibrariesFromTree: Framework project", async (t) => {
test.serial("utils.getFrameworkLibrariesFromTree: Framework project with framework dependency", async (t) => {
// Only root-level framework projects are scanned
const {utils} = t.context;
const dependencyTree = {
id: "@sapui5/project",
Expand All @@ -399,7 +518,7 @@ test.serial("utils.getFrameworkLibrariesFromTree: Framework project", async (t)
}
},
dependencies: [{
id: "@openui5/test1",
id: "@openui5/test1", // Will not be scanned
version: "1.2.3",
path: libraryEPath,
configuration: {
Expand All @@ -421,7 +540,7 @@ test.serial("utils.getFrameworkLibrariesFromTree: Framework project", async (t)
const projectGraph = await projectGraphBuilder(provider);

const ui5Dependencies = await utils.getFrameworkLibrariesFromGraph(projectGraph);
t.deepEqual(ui5Dependencies, []);
t.deepEqual(ui5Dependencies, ["lib1"]);
});

test.serial("utils.getFrameworkLibrariesFromTree: Project with libraries and dependency with libraries", async (t) => {
Expand Down

0 comments on commit f90d17a

Please sign in to comment.