Skip to content

Commit

Permalink
[FIX] Resolve UI5 data directory relative to project (#642)
Browse files Browse the repository at this point in the history
The UI5 data directory (previously called UI5 home directory) should
always be resolved relative to the project directory, which is where
the package.json file is located at.

This change also adds a list of options to the Configuration class to be
used by the CLI config command to allow all available config options to
be set or retrieved.

Follow-up of #635

JIRA: CPOUI5FOUNDATION-715
JIRA: CPOUI5FOUNDATION-716

---------

Co-authored-by: Merlin Beutlberger <m.beutlberger@sap.com>
  • Loading branch information
matz3 and RandomByte authored Aug 18, 2023
1 parent 23fd49d commit 228b14c
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 18 deletions.
39 changes: 27 additions & 12 deletions lib/config/Configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,43 @@ import path from "node:path";
import os from "node:os";

/**
* Provides basic configuration settings for @ui5/project/ui5Framework/* resolvers.
* Provides basic configuration for @ui5/project.
* Reads/writes configuration from/to ~/.ui5rc
*
* @public
* @class
* @alias @ui5/project/config/Configuration
*/
class Configuration {
#mavenSnapshotEndpointUrl;
#ui5DataDir;
/**
* A list of all configuration options.
*
* @public
* @static
*/
static OPTIONS = [
"mavenSnapshotEndpointUrl",
"ui5DataDir"
];

#options = new Map();

/**
* @param {object} configuration
* @param {string} [configuration.mavenSnapshotEndpointUrl]
* @param {string} [configuration.ui5DataDir]
*/
constructor({mavenSnapshotEndpointUrl, ui5DataDir}) {
this.#mavenSnapshotEndpointUrl = mavenSnapshotEndpointUrl;
this.#ui5DataDir = ui5DataDir;
constructor(configuration) {
// Initialize map with undefined values for every option so that they are
// returned via toJson()
Configuration.OPTIONS.forEach((key) => this.#options.set(key, undefined));

Object.entries(configuration).forEach(([key, value]) => {
if (!Configuration.OPTIONS.includes(key)) {
throw new Error(`Unknown configuration option '${key}'`);
}
this.#options.set(key, value);
});
}

/**
Expand All @@ -31,7 +49,7 @@ class Configuration {
* @returns {string}
*/
getMavenSnapshotEndpointUrl() {
return this.#mavenSnapshotEndpointUrl;
return this.#options.get("mavenSnapshotEndpointUrl");
}

/**
Expand All @@ -41,18 +59,15 @@ class Configuration {
* @returns {string}
*/
getUi5DataDir() {
return this.#ui5DataDir;
return this.#options.get("ui5DataDir");
}

/**
* @public
* @returns {object} The configuration in a JSON format
*/
toJson() {
return {
mavenSnapshotEndpointUrl: this.#mavenSnapshotEndpointUrl,
ui5DataDir: this.#ui5DataDir,
};
return Object.fromEntries(this.#options);
}

/**
Expand Down
14 changes: 9 additions & 5 deletions lib/graph/helpers/ui5Framework.js
Original file line number Diff line number Diff line change
Expand Up @@ -365,11 +365,15 @@ export default {
});
}

const config = await Configuration.fromFile();
// ENV var should take precedence over the dataDir from the configuration.
const ui5HomeDir = process.env.UI5_DATA_DIR ?
path.resolve(process.env.UI5_DATA_DIR) :
config.getUi5DataDir();
let ui5DataDir = process.env.UI5_DATA_DIR;
if (!ui5DataDir) {
const config = await Configuration.fromFile();
ui5DataDir = config.getUi5DataDir();
}
if (ui5DataDir) {
ui5DataDir = path.resolve(rootProject.getRootPath(), ui5DataDir);
}

// 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
Expand All @@ -378,7 +382,7 @@ export default {
version,
providedLibraryMetadata,
cacheMode,
ui5HomeDir
ui5HomeDir: ui5DataDir
});

let startTime;
Expand Down
21 changes: 20 additions & 1 deletion test/lib/config/Configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ test.afterEach.always((t) => {
esmock.purge(t.context.Configuration);
});

test.serial("Configuration options", (t) => {
const {Configuration} = t.context;
t.deepEqual(Configuration.OPTIONS, [
"mavenSnapshotEndpointUrl",
"ui5DataDir"
]);
});

test.serial("Build configuration with defaults", (t) => {
const {Configuration} = t.context;

Expand All @@ -39,7 +47,6 @@ test.serial("Build configuration with defaults", (t) => {
});
});


test.serial("Overwrite defaults defaults", (t) => {
const {Configuration} = t.context;

Expand All @@ -53,6 +60,18 @@ test.serial("Overwrite defaults defaults", (t) => {
t.deepEqual(config.toJson(), params);
});

test.serial("Unknown configuration option", (t) => {
const {Configuration} = t.context;

const params = {
unknown: "foo"
};

t.throws(() => new Configuration(params), {
message: `Unknown configuration option 'unknown'`
});
});

test.serial("Check getters", (t) => {
const {Configuration} = t.context;

Expand Down
8 changes: 8 additions & 0 deletions test/lib/graph/helpers/ui5Framework.integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,19 @@ test.beforeEach(async (t) => {
"../../../../lib/specifications/Specification.js": t.context.Specification
});

// Stub os homedir to prevent the actual ~/.ui5rc from being used in tests
t.context.Configuration = await esmock.p("../../../../lib/config/Configuration.js", {
"node:os": {
homedir: sinon.stub().returns(path.join(fakeBaseDir, "homedir"))
}
});

t.context.ui5Framework = await esmock.p("../../../../lib/graph/helpers/ui5Framework.js", {
"@ui5/logger": ui5Logger,
"../../../../lib/graph/Module.js": t.context.Module,
"../../../../lib/ui5Framework/Openui5Resolver.js": t.context.Openui5Resolver,
"../../../../lib/ui5Framework/Sapui5Resolver.js": t.context.Sapui5Resolver,
"../../../../lib/config/Configuration.js": t.context.Configuration
});

t.context.projectGraphBuilder = await esmock.p("../../../../lib/graph/projectGraphBuilder.js", {
Expand Down
187 changes: 187 additions & 0 deletions test/lib/graph/helpers/ui5Framework.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ const libraryEPath = path.join(__dirname, "..", "..", "..", "fixtures", "library
const libraryFPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.f");

test.beforeEach(async (t) => {
// Tests either rely on not having UI5_DATA_DIR defined, or explicitly define it
t.context.originalUi5DataDirEnv = process.env.UI5_DATA_DIR;
delete process.env.UI5_DATA_DIR;

const sinon = t.context.sinon = sinonGlobal.createSandbox();

t.context.log = {
Expand Down Expand Up @@ -51,15 +55,30 @@ test.beforeEach(async (t) => {
t.context.Sapui5MavenSnapshotResolverResolveVersionStub = sinon.stub();
t.context.Sapui5MavenSnapshotResolverStub.resolveVersion = t.context.Sapui5MavenSnapshotResolverResolveVersionStub;

t.context.getUi5DataDirStub = sinon.stub().returns(undefined);

t.context.ConfigurationStub = {
fromFile: sinon.stub().resolves({
getUi5DataDir: t.context.getUi5DataDirStub
})
};

t.context.ui5Framework = await esmock.p("../../../../lib/graph/helpers/ui5Framework.js", {
"@ui5/logger": ui5Logger,
"../../../../lib/ui5Framework/Sapui5Resolver.js": t.context.Sapui5ResolverStub,
"../../../../lib/ui5Framework/Sapui5MavenSnapshotResolver.js": t.context.Sapui5MavenSnapshotResolverStub,
"../../../../lib/config/Configuration.js": t.context.ConfigurationStub,
});
t.context.utils = t.context.ui5Framework._utils;
});

test.afterEach.always((t) => {
// Reset UI5_DATA_DIR env
if (typeof t.context.originalUi5DataDirEnv === "undefined") {
delete process.env.UI5_DATA_DIR;
} else {
process.env.UI5_DATA_DIR = t.context.originalUi5DataDirEnv;
}
t.context.sinon.restore();
esmock.purge(t.context.ui5Framework);
});
Expand Down Expand Up @@ -1020,6 +1039,174 @@ test.serial("enrichProjectGraph should allow omitting framework version in case
t.is(Sapui5ResolverStub.getCall(0).args[0].providedLibraryMetadata, workspaceFrameworkLibraryMetadata);
});

test.serial("enrichProjectGraph should use UI5 data dir from env var", async (t) => {
const {sinon, ui5Framework, utils, Sapui5ResolverInstallStub} = t.context;

const dependencyTree = {
id: "test1",
version: "1.0.0",
path: applicationAPath,
configuration: {
specVersion: "2.0",
type: "application",
metadata: {
name: "application.a"
},
framework: {
name: "SAPUI5",
version: "1.75.0"
}
}
};

const referencedLibraries = ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib3"];
const libraryMetadata = {fake: "metadata"};

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);

process.env.UI5_DATA_DIR = "./ui5-data-dir-from-env-var";

const expectedUi5DataDir = path.resolve(dependencyTree.path, "./ui5-data-dir-from-env-var");

await ui5Framework.enrichProjectGraph(projectGraph);

t.is(t.context.Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once");
t.deepEqual(t.context.Sapui5ResolverStub.getCall(0).args, [{
cacheMode: undefined,
cwd: dependencyTree.path,
version: dependencyTree.configuration.framework.version,
ui5HomeDir: expectedUi5DataDir,
providedLibraryMetadata: undefined
}], "Sapui5Resolver#constructor should be called with expected args");
});

test.serial("enrichProjectGraph should use UI5 data dir from configuration", async (t) => {
const {sinon, ui5Framework, utils, Sapui5ResolverInstallStub, getUi5DataDirStub} = t.context;

const dependencyTree = {
id: "test1",
version: "1.0.0",
path: applicationAPath,
configuration: {
specVersion: "2.0",
type: "application",
metadata: {
name: "application.a"
},
framework: {
name: "SAPUI5",
version: "1.75.0"
}
}
};

const referencedLibraries = ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib3"];
const libraryMetadata = {fake: "metadata"};

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);

getUi5DataDirStub.returns("./ui5-data-dir-from-config");

const expectedUi5DataDir = path.resolve(dependencyTree.path, "./ui5-data-dir-from-config");

await ui5Framework.enrichProjectGraph(projectGraph);

t.is(t.context.Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once");
t.deepEqual(t.context.Sapui5ResolverStub.getCall(0).args, [{
cacheMode: undefined,
cwd: dependencyTree.path,
version: dependencyTree.configuration.framework.version,
ui5HomeDir: expectedUi5DataDir,
providedLibraryMetadata: undefined
}], "Sapui5Resolver#constructor should be called with expected args");
});

test.serial("enrichProjectGraph should use absolute UI5 data dir from configuration", async (t) => {
const {sinon, ui5Framework, utils, Sapui5ResolverInstallStub, getUi5DataDirStub} = t.context;

const dependencyTree = {
id: "test1",
version: "1.0.0",
path: applicationAPath,
configuration: {
specVersion: "2.0",
type: "application",
metadata: {
name: "application.a"
},
framework: {
name: "SAPUI5",
version: "1.75.0"
}
}
};

const referencedLibraries = ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib3"];
const libraryMetadata = {fake: "metadata"};

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);

getUi5DataDirStub.returns("/absolute-ui5-data-dir-from-config");

const expectedUi5DataDir = path.resolve("/absolute-ui5-data-dir-from-config");

await ui5Framework.enrichProjectGraph(projectGraph);

t.is(t.context.Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once");
t.deepEqual(t.context.Sapui5ResolverStub.getCall(0).args, [{
cacheMode: undefined,
cwd: dependencyTree.path,
version: dependencyTree.configuration.framework.version,
ui5HomeDir: expectedUi5DataDir,
providedLibraryMetadata: undefined
}], "Sapui5Resolver#constructor should be called with expected args");
});

test.serial("utils.shouldIncludeDependency", (t) => {
const {utils} = t.context;
// root project dependency should always be included
Expand Down

0 comments on commit 228b14c

Please sign in to comment.