Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FIX] Resolve UI5 data directory relative to project #642

Merged
merged 5 commits into from
Aug 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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