diff --git a/internal/documentation/docs/updates/migrate-v5.md b/internal/documentation/docs/updates/migrate-v5.md index c9db7b58dac..bbb66f25ac5 100644 --- a/internal/documentation/docs/updates/migrate-v5.md +++ b/internal/documentation/docs/updates/migrate-v5.md @@ -27,6 +27,8 @@ Or update your global install via: `npm i --global @ui5/cli@next` - **@ui5/cli: `ui5 serve` renders a status banner in interactive terminals** +- **@ui5/project: UI5 framework resolver constructors now require explicit `ui5DataDir`** + ## Node.js and npm Version Support @@ -107,6 +109,46 @@ If you previously passed any of these options to a command that did not use them The `ui5 init` command now generates projects with Specification Version 5.0 by default. +## Changes to @ui5/project (Node.js API) + +When consuming the Node.js API, UI5 framework resolver constructors now require the `ui5DataDir` option. +This affects `Openui5Resolver`, `Sapui5Resolver`, and `Sapui5MavenSnapshotResolver`. + +Previously, `ui5DataDir` was optional and resolver constructors implicitly resolved a fallback from +environment/configuration. In UI5 CLI v5, callers must resolve the UI5 data directory before constructing a +resolver and pass it explicitly. This change improves API clarity by making the dependency explicit. + +Use [`resolveUi5DataDir`](../api/module-@ui5_project_utils_dataDir.md) to resolve the path once at your async +entry boundary and forward the resolved value to all APIs that need it. + +::: code-group +```js [Before] +import Sapui5Resolver from "@ui5/project/ui5Framework/Sapui5Resolver"; + +const resolver = new Sapui5Resolver({ + cwd: process.cwd(), + version: "1.120.15" +}); +``` + +```js [After] +import Sapui5Resolver from "@ui5/project/ui5Framework/Sapui5Resolver"; +import {resolveUi5DataDir} from "@ui5/project/utils/dataDir"; + +async function createResolver() { + const ui5DataDir = await resolveUi5DataDir({projectRootPath: process.cwd()}); + + const resolver = new Sapui5Resolver({ + cwd: process.cwd(), + version: "1.120.15", + ui5DataDir + }); + + return resolver; +} +``` +::: + ## Component Type The `component` type feature aims to introduce a new project type within the UI5 CLI ecosystem to support the development of UI5 component-like applications intended to run in container apps such as the Fiori Launchpad (FLP) Sandbox or testsuite environments. diff --git a/packages/cli/lib/framework/utils.js b/packages/cli/lib/framework/utils.js index 799c8a35253..d5e272eda05 100644 --- a/packages/cli/lib/framework/utils.js +++ b/packages/cli/lib/framework/utils.js @@ -1,6 +1,5 @@ -import path from "node:path"; import {graphFromStaticFile, graphFromPackageDependencies} from "@ui5/project/graph"; -import Configuration from "@ui5/project/config/Configuration"; +import {resolveUi5DataDir} from "@ui5/project/utils/dataDir"; export async function getRootProjectConfiguration(projectGraphOptions) { let graph; @@ -37,7 +36,7 @@ export async function createFrameworkResolverInstance({frameworkName, frameworkV return new Resolver({ cwd, version: frameworkVersion, - ui5DataDir: await utils.getUi5DataDir({cwd}) + ui5DataDir: await resolveUi5DataDir({projectRootPath: cwd}) }); } @@ -45,26 +44,15 @@ export async function frameworkResolverResolveVersion({frameworkName, frameworkV const Resolver = await utils.getFrameworkResolver(frameworkName, frameworkVersion); return Resolver.resolveVersion(frameworkVersion, { cwd, - ui5DataDir: await utils.getUi5DataDir({cwd}) + ui5DataDir: await resolveUi5DataDir({projectRootPath: cwd}) }); } -async function getUi5DataDir({cwd}) { - // ENV var should take precedence over the dataDir from the configuration. - let ui5DataDir = process.env.UI5_DATA_DIR; - if (!ui5DataDir) { - const config = await Configuration.fromFile(); - ui5DataDir = config.getUi5DataDir(); - } - return ui5DataDir ? path.resolve(cwd, ui5DataDir) : undefined; -} - const utils = { getRootProjectConfiguration, getFrameworkResolver, createFrameworkResolverInstance, frameworkResolverResolveVersion, - getUi5DataDir }; let _utils; // For mocking of functions in unit tests and testing internal functions diff --git a/packages/cli/test/lib/framework/utils.js b/packages/cli/test/lib/framework/utils.js index 0ac6f4aaf37..48c7a4ba990 100644 --- a/packages/cli/test/lib/framework/utils.js +++ b/packages/cli/test/lib/framework/utils.js @@ -1,7 +1,6 @@ import test from "ava"; import sinonGlobal from "sinon"; import esmock from "esmock"; -import path from "node:path"; test.beforeEach(async (t) => { // Tests either rely on not having UI5_DATA_DIR defined, or explicitly define it @@ -21,12 +20,7 @@ test.beforeEach(async (t) => { t.context.Openui5Resolver = sinon.stub(); t.context.Sapui5MavenSnapshotResolver = sinon.stub(); - t.context.ConfigurationGetUi5DataDirStub = sinon.stub().returns(undefined); - t.context.ConfigurationStub = { - fromFile: sinon.stub().resolves({ - getUi5DataDir: t.context.ConfigurationGetUi5DataDirStub - }) - }; + t.context.resolveUi5DataDirStub = sinon.stub().resolves("my-default-ui5-data-dir"); t.context.utils = await esmock.p("../../../lib/framework/utils.js", { "@ui5/project/graph": { @@ -42,7 +36,9 @@ test.beforeEach(async (t) => { "@ui5/project/ui5Framework/Sapui5MavenSnapshotResolver": { default: t.context.Sapui5MavenSnapshotResolver }, - "@ui5/project/config/Configuration": t.context.ConfigurationStub + "@ui5/project/utils/dataDir": { + resolveUi5DataDir: t.context.resolveUi5DataDirStub + } }); t.context._utils = t.context.utils._utils; }); @@ -142,13 +138,13 @@ test.serial("getFrameworkResolver: Invalid framework.name", async (t) => { }); }); -test.serial("createFrameworkResolverInstance: Without ui5DataDir", async (t) => { +test.serial("createFrameworkResolverInstance: Without explicit ui5DataDir (uses resolved default)", async (t) => { const {createFrameworkResolverInstance} = t.context.utils; - const {sinon} = t.context; + const {sinon, resolveUi5DataDirStub} = t.context; const ResolverStub = sinon.stub().returns({}); sinon.stub(t.context._utils, "getFrameworkResolver").resolves(ResolverStub); - sinon.stub(t.context._utils, "getUi5DataDir").resolves(undefined); + resolveUi5DataDirStub.resolves("my-default-ui5-data-dir"); const result = await createFrameworkResolverInstance({ frameworkName: "", @@ -163,12 +159,7 @@ test.serial("createFrameworkResolverInstance: Without ui5DataDir", async (t) => "" ]); - t.is(t.context._utils.getUi5DataDir.callCount, 1); - t.deepEqual(t.context._utils.getUi5DataDir.getCall(0).args, [ - { - cwd: "my-project-path" - } - ]); + t.is(resolveUi5DataDirStub.callCount, 1); t.is(ResolverStub.callCount, 1); t.is(result, ResolverStub.getCall(0).returnValue); @@ -177,18 +168,18 @@ test.serial("createFrameworkResolverInstance: Without ui5DataDir", async (t) => { cwd: "my-project-path", version: "", - ui5DataDir: undefined + ui5DataDir: "my-default-ui5-data-dir" } ]); }); test.serial("createFrameworkResolverInstance: With ui5DataDir", async (t) => { const {createFrameworkResolverInstance} = t.context.utils; - const {sinon} = t.context; + const {sinon, resolveUi5DataDirStub} = t.context; const ResolverStub = sinon.stub().returns({}); sinon.stub(t.context._utils, "getFrameworkResolver").resolves(ResolverStub); - sinon.stub(t.context._utils, "getUi5DataDir").resolves("my-ui5-data-dir"); + resolveUi5DataDirStub.resolves("my-ui5-data-dir"); const result = await createFrameworkResolverInstance({ frameworkName: "", @@ -203,12 +194,7 @@ test.serial("createFrameworkResolverInstance: With ui5DataDir", async (t) => { "" ]); - t.is(t.context._utils.getUi5DataDir.callCount, 1); - t.deepEqual(t.context._utils.getUi5DataDir.getCall(0).args, [ - { - cwd: "my-project-path" - } - ]); + t.is(resolveUi5DataDirStub.callCount, 1); t.is(ResolverStub.callCount, 1); t.is(result, ResolverStub.getCall(0).returnValue); @@ -224,13 +210,13 @@ test.serial("createFrameworkResolverInstance: With ui5DataDir", async (t) => { test.serial("frameworkResolverResolveVersion", async (t) => { const {frameworkResolverResolveVersion} = t.context.utils; - const {sinon} = t.context; + const {sinon, resolveUi5DataDirStub} = t.context; const resolveVersionStub = sinon.stub().resolves("1.111.1"); sinon.stub(t.context._utils, "getFrameworkResolver").resolves({ resolveVersion: resolveVersionStub }); - sinon.stub(t.context._utils, "getUi5DataDir").resolves(undefined); + resolveUi5DataDirStub.resolves("my-default-ui5-data-dir"); const result = await frameworkResolverResolveVersion({ frameworkName: "SAPUI5", @@ -246,52 +232,7 @@ test.serial("frameworkResolverResolveVersion", async (t) => { "latest", { cwd: "my-project-path", - ui5DataDir: undefined + ui5DataDir: "my-default-ui5-data-dir" } ]); }); - -test.serial("getUi5DataDir: no value defined", async (t) => { - const {ConfigurationGetUi5DataDirStub} = t.context; - const {getUi5DataDir} = t.context._utils; - - const result = await getUi5DataDir({ - cwd: path.resolve("foo") - }); - - t.is(result, undefined); - - t.is(ConfigurationGetUi5DataDirStub.callCount, 1); -}); - -test.serial("getUi5DataDir: from environment variable", async (t) => { - const {ConfigurationGetUi5DataDirStub} = t.context; - const {getUi5DataDir} = t.context._utils; - - // Environment variable must be preferred over configuration value - ConfigurationGetUi5DataDirStub.returns(".ui5-data-dir-from-configuration"); - process.env.UI5_DATA_DIR = ".ui5-data-dir-from-env-variable"; - - const result = await getUi5DataDir({ - cwd: path.resolve("foo") - }); - - t.is(result, path.join(path.resolve("foo"), ".ui5-data-dir-from-env-variable")); - - t.is(ConfigurationGetUi5DataDirStub.callCount, 0); -}); - -test.serial("getUi5DataDir: from Configuration", async (t) => { - const {ConfigurationGetUi5DataDirStub} = t.context; - const {getUi5DataDir} = t.context._utils; - - ConfigurationGetUi5DataDirStub.returns(".ui5-data-dir-from-configuration"); - - const result = await getUi5DataDir({ - cwd: path.resolve("foo") - }); - - t.is(result, path.join(path.resolve("foo"), ".ui5-data-dir-from-configuration")); - - t.is(ConfigurationGetUi5DataDirStub.callCount, 1); -}); diff --git a/packages/project/lib/build/cache/CacheManager.js b/packages/project/lib/build/cache/CacheManager.js index e1e37a6f521..a4b5f12aad4 100644 --- a/packages/project/lib/build/cache/CacheManager.js +++ b/packages/project/lib/build/cache/CacheManager.js @@ -1,8 +1,7 @@ import path from "node:path"; -import os from "node:os"; -import Configuration from "../../config/Configuration.js"; import {getLogger} from "@ui5/logger"; import BuildCacheStorage from "./BuildCacheStorage.js"; +import os from "node:os"; const log = getLogger("build:cache:CacheManager"); @@ -60,31 +59,20 @@ export default class CacheManager { * Returns a singleton CacheManager for the determined cache directory. * The cache directory is resolved in this order: * 1. Explicit ui5DataDir option (resolved relative to cwd) - * 2. UI5_DATA_DIR environment variable (resolved relative to cwd) - * 3. ui5DataDir from UI5 configuration file - * 4. Default: ~/.ui5/ + * 2. UI5_DATA_DIR environment variable or ui5DataDir from configuration file + * (resolved relative to cwd via {@link resolveUi5DataDir}) + * 3. Default: ~/.ui5/ * * @public - * @param {string} cwd Current working directory for resolving relative paths * @param {object} [options] * @param {string} [options.ui5DataDir] Explicit UI5 data directory. When provided, * environment variable, configuration file, and home-directory fallbacks are skipped. * @returns {Promise} Singleton CacheManager instance for the cache directory */ - static async create(cwd, {ui5DataDir} = {}) { - if (!ui5DataDir) { - // ENV var should take precedence over the dataDir from the configuration. - ui5DataDir = process.env.UI5_DATA_DIR; - if (!ui5DataDir) { - const config = await Configuration.fromFile(); - ui5DataDir = config.getUi5DataDir(); - } - } - if (ui5DataDir) { - ui5DataDir = path.resolve(cwd, ui5DataDir); - } else { - ui5DataDir = path.join(os.homedir(), ".ui5"); - } + static async create({ui5DataDir} = {}) { + ui5DataDir = path.resolve( + ui5DataDir || path.join(os.homedir(), ".ui5") + ); const cacheDir = path.join(ui5DataDir, "buildCache"); log.verbose(`Using build cache directory: ${cacheDir}`); diff --git a/packages/project/lib/build/helpers/BuildContext.js b/packages/project/lib/build/helpers/BuildContext.js index 50251ac9ddc..5a030cc728f 100644 --- a/packages/project/lib/build/helpers/BuildContext.js +++ b/packages/project/lib/build/helpers/BuildContext.js @@ -5,6 +5,7 @@ import {getBaseSignature} from "./getBuildSignature.js"; import {getLogger} from "@ui5/logger"; const log = getLogger("build:helpers:BuildContext"); import Cache from "../cache/Cache.js"; +import {resolveUi5DataDir} from "../../utils/dataDir.js"; /** * Context of a build process @@ -170,8 +171,9 @@ class BuildContext { if (this.#cacheManager) { return this.#cacheManager; } - this.#cacheManager = await CacheManager.create(this._graph.getRoot().getRootPath(), { - ui5DataDir: this._ui5DataDir, + this.#cacheManager = await CacheManager.create({ + ui5DataDir: this._ui5DataDir ?? + await resolveUi5DataDir({projectRootPath: this.getRootProject().getRootPath()}), }); return this.#cacheManager; } diff --git a/packages/project/lib/graph/helpers/ui5Framework.js b/packages/project/lib/graph/helpers/ui5Framework.js index 660cc78427e..06921dc5b23 100644 --- a/packages/project/lib/graph/helpers/ui5Framework.js +++ b/packages/project/lib/graph/helpers/ui5Framework.js @@ -2,8 +2,7 @@ import Module from "../Module.js"; import ProjectGraph from "../ProjectGraph.js"; import {getLogger} from "@ui5/logger"; const log = getLogger("graph:helpers:ui5Framework"); -import Configuration from "../../config/Configuration.js"; -import path from "node:path"; +import {resolveUi5DataDir} from "../../utils/dataDir.js"; class ProjectProcessor { constructor({libraryMetadata, graph, workspace}) { @@ -348,15 +347,9 @@ export default { Resolver = (await import("../../ui5Framework/Sapui5Resolver.js")).default; } - // ENV var should take precedence over the dataDir from the configuration. - let ui5DataDir = process.env.UI5_DATA_DIR; - if (!ui5DataDir) { - const config = await Configuration.fromFile(); - ui5DataDir = config.getUi5DataDir(); - } - if (ui5DataDir) { - ui5DataDir = path.resolve(cwd, ui5DataDir); - } + // Resolve the UI5 data directory using the shared utility. + // Returns ~/.ui5 by default when no env var or configuration is set. + const ui5DataDir = await resolveUi5DataDir({projectRootPath: cwd}); if (options.versionOverride) { version = await Resolver.resolveVersion(options.versionOverride, { diff --git a/packages/project/lib/ui5Framework/AbstractResolver.js b/packages/project/lib/ui5Framework/AbstractResolver.js index 266d4bcd1a6..fb2aaf0c46e 100644 --- a/packages/project/lib/ui5Framework/AbstractResolver.js +++ b/packages/project/lib/ui5Framework/AbstractResolver.js @@ -1,5 +1,4 @@ import path from "node:path"; -import os from "node:os"; import {getLogger} from "@ui5/logger"; const log = getLogger("ui5Framework:AbstractResolver"); import semver from "semver"; @@ -26,8 +25,8 @@ class AbstractResolver { * @param {boolean} [options.sources=false] Whether to install framework libraries as sources or * pre-built (with build manifest) * @param {string} [options.cwd=process.cwd()] Current working directory - * @param {string} [options.ui5DataDir="~/.ui5"] UI5 home directory location. This will be used to store packages, - * metadata and configuration used by the resolvers. Relative to `process.cwd()` + * @param {string} options.ui5DataDir Resolved UI5 home directory location. This is used to store + * metadata and packages used by the resolvers and must be resolved by the caller. * @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. @@ -38,12 +37,12 @@ class AbstractResolver { if (new.target === AbstractResolver) { throw new TypeError("Class 'AbstractResolver' is abstract"); } + if (!ui5DataDir) { + const resolverName = new.target?.name || "AbstractResolver"; + throw new Error(`${resolverName}: Missing parameter "ui5DataDir"`); + } - // In some CI environments, the homedir might be set explicitly to a relative - // path (e.g. "./"), but tooling requires an absolute path - this._ui5DataDir = path.resolve( - ui5DataDir || path.join(os.homedir(), ".ui5") - ); + this._ui5DataDir = path.resolve(ui5DataDir); this._cwd = cwd ? path.resolve(cwd) : process.cwd(); this._version = version; @@ -174,9 +173,12 @@ class AbstractResolver { * Installs the provided libraries and their dependencies * * ```js - * const resolver = new Sapui5Resolver({version: "1.76.0"}); + * const resolver = new Sapui5Resolver({ + * version: "1.76.0", + * ui5DataDir: "/path/to/.ui5" + * }); * // Or for OpenUI5: - * // const resolver = new Openui5Resolver({version: "1.76.0"}); + * // const resolver = new Openui5Resolver({version: "1.76.0", ui5DataDir: "/path/to/.ui5"}); * * resolver.install(["sap.ui.core", "sap.m"]).then(({libraryMetadata}) => { * // Installation done diff --git a/packages/project/lib/ui5Framework/Openui5Resolver.js b/packages/project/lib/ui5Framework/Openui5Resolver.js index a6a9c4fc02a..72eefe02a3e 100644 --- a/packages/project/lib/ui5Framework/Openui5Resolver.js +++ b/packages/project/lib/ui5Framework/Openui5Resolver.js @@ -18,8 +18,8 @@ class Openui5Resolver extends AbstractResolver { * @param {*} options options * @param {string} options.version OpenUI5 version to use * @param {string} [options.cwd=process.cwd()] Working directory to resolve configurations like .npmrc - * @param {string} [options.ui5DataDir="~/.ui5"] UI5 home directory location. This will be used to store packages, - * metadata and configuration used by the resolvers. Relative to `process.cwd()` + * @param {string} options.ui5DataDir Resolved UI5 home directory location. This is used to + * store metadata and packages used by the resolvers. * @param {string} [options.cacheDir] Where to store temp/cached packages. * @param {string} [options.packagesDir] Where to install packages * @param {string} [options.stagingDir] The staging directory for the packages diff --git a/packages/project/lib/ui5Framework/Sapui5MavenSnapshotResolver.js b/packages/project/lib/ui5Framework/Sapui5MavenSnapshotResolver.js index 01c4b843541..c56b4f7cba0 100644 --- a/packages/project/lib/ui5Framework/Sapui5MavenSnapshotResolver.js +++ b/packages/project/lib/ui5Framework/Sapui5MavenSnapshotResolver.js @@ -32,8 +32,8 @@ class Sapui5MavenSnapshotResolver extends AbstractResolver { * @param {boolean} [options.sources=false] Whether to install framework libraries as sources or * pre-built (with build manifest) * @param {string} [options.cwd=process.cwd()] Current working directory - * @param {string} [options.ui5DataDir="~/.ui5"] UI5 home directory location. This will be used to store packages, - * metadata and configuration used by the resolvers. Relative to `process.cwd()` + * @param {string} options.ui5DataDir Resolved UI5 home directory location. This is used to + * store metadata and packages used by the resolvers. * @param {module:@ui5/project/ui5Framework/maven/SnapshotCache} [options.snapshotCache=Default] * Snapshot cache mode to use */ diff --git a/packages/project/lib/ui5Framework/Sapui5Resolver.js b/packages/project/lib/ui5Framework/Sapui5Resolver.js index 300020dce25..0593cf734d7 100644 --- a/packages/project/lib/ui5Framework/Sapui5Resolver.js +++ b/packages/project/lib/ui5Framework/Sapui5Resolver.js @@ -21,8 +21,8 @@ class Sapui5Resolver extends AbstractResolver { * @param {*} options options * @param {string} options.version SAPUI5 version to use * @param {string} [options.cwd=process.cwd()] Working directory to resolve configurations like .npmrc - * @param {string} [options.ui5DataDir="~/.ui5"] UI5 home directory location. This will be used to store packages, - * metadata and configuration used by the resolvers. Relative to `process.cwd()` + * @param {string} options.ui5DataDir Resolved UI5 home directory location. This is used to + * store metadata and packages used by the resolvers. * @param {string} [options.cacheDir] Where to store temp/cached packages. * @param {string} [options.packagesDir] Where to install packages * @param {string} [options.stagingDir] The staging directory for packages @@ -75,7 +75,8 @@ class Sapui5Resolver extends AbstractResolver { const {default: Openui5Resolver} = await import("./Openui5Resolver.js"); const openui5Resolver = new Openui5Resolver({ cwd: this._cwd, - version: metadata.version + version: metadata.version, + ui5DataDir: this._ui5DataDir }); const openui5Metadata = await openui5Resolver.getLibraryMetadata(libraryName); return { diff --git a/packages/project/lib/utils/dataDir.js b/packages/project/lib/utils/dataDir.js new file mode 100644 index 00000000000..fd0ae59a6b8 --- /dev/null +++ b/packages/project/lib/utils/dataDir.js @@ -0,0 +1,42 @@ +import path from "node:path"; +import os from "node:os"; +import Configuration from "../config/Configuration.js"; + +/** + * Utilities for resolving the UI5 data directory. + * + * @public + * @module @ui5/project/utils/dataDir + */ + +/** + * Resolves the UI5 data directory using the standard precedence chain: + *
    + *
  1. UI5_DATA_DIR environment variable
  2. + *
  3. ui5DataDir option from the configuration file (~/.ui5rc)
  4. + *
  5. Default: ~/.ui5
  6. + *
+ * + * This function always returns an absolute path — never undefined. + * + * @public + * @param {object} [options] + * @param {string} [options.projectRootPath] The root directory of the project being processed. + * Used to resolve a relative ui5DataDir value from the environment variable or + * configuration file against the correct base. This is NOT necessarily the shell's current + * working directory — when processing a project in a sub-directory or a workspace member, + * this should be the root of that specific project (where its package.json lives). + * Defaults to process.cwd() when not provided. + * @returns {Promise} Resolved absolute path to the UI5 data directory + */ +export async function resolveUi5DataDir({projectRootPath} = {}) { + let ui5DataDir = process.env.UI5_DATA_DIR; + if (!ui5DataDir) { + const config = await Configuration.fromFile(); + ui5DataDir = config.getUi5DataDir(); + } + if (ui5DataDir) { + return path.resolve(projectRootPath ?? process.cwd(), ui5DataDir); + } + return path.resolve(os.homedir(), ".ui5"); +} diff --git a/packages/project/package.json b/packages/project/package.json index 313bc5db619..394b6a269aa 100644 --- a/packages/project/package.json +++ b/packages/project/package.json @@ -27,6 +27,7 @@ "./ui5Framework/Sapui5Resolver": "./lib/ui5Framework/Sapui5Resolver.js", "./ui5Framework/maven/SnapshotCache": "./lib/ui5Framework/maven/SnapshotCache.js", "./validation/validator": "./lib/validation/validator.js", + "./utils/dataDir": "./lib/utils/dataDir.js", "./validation/ValidationError": "./lib/validation/ValidationError.js", "./graph/ProjectGraph": "./lib/graph/ProjectGraph.js", "./graph/projectGraphBuilder": "./lib/graph/projectGraphBuilder.js", diff --git a/packages/project/test/lib/graph/helpers/ui5Framework.integration.js b/packages/project/test/lib/graph/helpers/ui5Framework.integration.js index 93096d50109..13d867c969b 100644 --- a/packages/project/test/lib/graph/helpers/ui5Framework.integration.js +++ b/packages/project/test/lib/graph/helpers/ui5Framework.integration.js @@ -135,7 +135,10 @@ test.beforeEach(async (t) => { "../../../../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 + "../../../../lib/config/Configuration.js": t.context.Configuration, + "../../../../lib/utils/dataDir.js": { + resolveUi5DataDir: sinon.stub().resolves(path.join(fakeBaseDir, "homedir", ".ui5")) + } }); t.context.projectGraphBuilder = await esmock.p("../../../../lib/graph/projectGraphBuilder.js", { diff --git a/packages/project/test/lib/graph/helpers/ui5Framework.js b/packages/project/test/lib/graph/helpers/ui5Framework.js index b134ac187ac..3d9e91a7820 100644 --- a/packages/project/test/lib/graph/helpers/ui5Framework.js +++ b/packages/project/test/lib/graph/helpers/ui5Framework.js @@ -54,7 +54,7 @@ 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.getUi5DataDirStub = sinon.stub().returns(path.resolve("fake-ui5-data-dir")); t.context.ConfigurationStub = { fromFile: sinon.stub().resolves({ @@ -66,6 +66,7 @@ test.beforeEach(async (t) => { "@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; @@ -131,7 +132,7 @@ test.serial("enrichProjectGraph", async (t) => { snapshotCache: undefined, cwd: dependencyTree.path, version: dependencyTree.configuration.framework.version, - ui5DataDir: undefined, + ui5DataDir: path.resolve("fake-ui5-data-dir"), providedLibraryMetadata: undefined }], "Sapui5Resolver#constructor should be called with expected args"); @@ -336,7 +337,7 @@ test.serial("enrichProjectGraph: With versionOverride", async (t) => { t.is(Sapui5ResolverResolveVersionStub.callCount, 1); t.deepEqual(Sapui5ResolverResolveVersionStub.getCall(0).args, ["1.99", { cwd: dependencyTree.path, - ui5DataDir: undefined, + ui5DataDir: path.resolve("fake-ui5-data-dir"), }]); t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); @@ -344,7 +345,7 @@ test.serial("enrichProjectGraph: With versionOverride", async (t) => { snapshotCache: undefined, cwd: dependencyTree.path, version: "1.99.9", - ui5DataDir: undefined, + ui5DataDir: path.resolve("fake-ui5-data-dir"), providedLibraryMetadata: undefined }], "Sapui5Resolver#constructor should be called with expected args"); }); @@ -398,7 +399,7 @@ test.serial("enrichProjectGraph: With versionOverride containing snapshot versio t.is(Sapui5MavenSnapshotResolverResolveVersionStub.callCount, 1); t.deepEqual(Sapui5MavenSnapshotResolverResolveVersionStub.getCall(0).args, ["1.99-SNAPSHOT", { cwd: dependencyTree.path, - ui5DataDir: undefined, + ui5DataDir: path.resolve("fake-ui5-data-dir"), }]); t.is(Sapui5MavenSnapshotResolverStub.callCount, 1, @@ -407,7 +408,7 @@ test.serial("enrichProjectGraph: With versionOverride containing snapshot versio snapshotCache: undefined, cwd: dependencyTree.path, version: "1.99.9-SNAPSHOT", - ui5DataDir: undefined, + ui5DataDir: path.resolve("fake-ui5-data-dir"), providedLibraryMetadata: undefined }], "Sapui5Resolver#constructor should be called with expected args"); }); @@ -461,7 +462,7 @@ test.serial("enrichProjectGraph: With versionOverride containing latest-snapshot t.is(Sapui5MavenSnapshotResolverResolveVersionStub.callCount, 1); t.deepEqual(Sapui5MavenSnapshotResolverResolveVersionStub.getCall(0).args, ["latest-snapshot", { cwd: dependencyTree.path, - ui5DataDir: undefined, + ui5DataDir: path.resolve("fake-ui5-data-dir"), }]); t.is(Sapui5MavenSnapshotResolverStub.callCount, 1, @@ -470,7 +471,7 @@ test.serial("enrichProjectGraph: With versionOverride containing latest-snapshot snapshotCache: undefined, cwd: dependencyTree.path, version: "1.99.9-SNAPSHOT", - ui5DataDir: undefined, + ui5DataDir: path.resolve("fake-ui5-data-dir"), providedLibraryMetadata: undefined }], "Sapui5Resolver#constructor should be called with expected args"); }); @@ -630,7 +631,7 @@ test.serial("enrichProjectGraph should resolve framework project with version an snapshotCache: undefined, cwd: dependencyTree.path, version: "1.2.3", - ui5DataDir: undefined, + ui5DataDir: path.resolve("fake-ui5-data-dir"), providedLibraryMetadata: undefined }], "Sapui5Resolver#constructor should be called with expected args"); }); @@ -726,7 +727,7 @@ test.serial("enrichProjectGraph should resolve framework project " + t.is(Sapui5ResolverResolveVersionStub.callCount, 1); t.deepEqual(Sapui5ResolverResolveVersionStub.getCall(0).args, ["3.4.5", { cwd: dependencyTree.path, - ui5DataDir: undefined, + ui5DataDir: path.resolve("fake-ui5-data-dir"), }]); t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); @@ -735,7 +736,7 @@ test.serial("enrichProjectGraph should resolve framework project " + snapshotCache: undefined, cwd: dependencyTree.path, version: "1.99.9", - ui5DataDir: undefined, + ui5DataDir: path.resolve("fake-ui5-data-dir"), providedLibraryMetadata: undefined }], "Sapui5Resolver#constructor should be called with expected args"); }); @@ -1000,7 +1001,7 @@ test.serial("enrichProjectGraph should use framework library metadata from works snapshotCache: undefined, cwd: dependencyTree.path, version: "1.111.1", - ui5DataDir: undefined, + ui5DataDir: path.resolve("fake-ui5-data-dir"), providedLibraryMetadata: workspaceFrameworkLibraryMetadata }], "Sapui5Resolver#constructor should be called with expected args"); t.is(Sapui5ResolverStub.getCall(0).args[0].providedLibraryMetadata, workspaceFrameworkLibraryMetadata); @@ -1058,7 +1059,7 @@ test.serial("enrichProjectGraph should allow omitting framework version in case t.deepEqual(Sapui5ResolverStub.getCall(0).args, [{ snapshotCache: undefined, cwd: dependencyTree.path, - ui5DataDir: undefined, + ui5DataDir: path.resolve("fake-ui5-data-dir"), version: undefined, providedLibraryMetadata: workspaceFrameworkLibraryMetadata }], "Sapui5Resolver#constructor should be called with expected args"); diff --git a/packages/project/test/lib/package-exports.js b/packages/project/test/lib/package-exports.js index 684e8634a84..10472adbf86 100644 --- a/packages/project/test/lib/package-exports.js +++ b/packages/project/test/lib/package-exports.js @@ -13,7 +13,7 @@ test("export of package.json", (t) => { // Check number of definied exports test("check number of exports", (t) => { const packageJson = require("@ui5/project/package.json"); - t.is(Object.keys(packageJson.exports).length, 14); + t.is(Object.keys(packageJson.exports).length, 15); }); // Public API contract (exported modules) @@ -26,6 +26,7 @@ test("check number of exports", (t) => { "ui5Framework/Sapui5Resolver", "ui5Framework/Sapui5MavenSnapshotResolver", "ui5Framework/maven/SnapshotCache", + "utils/dataDir", "validation/validator", "validation/ValidationError", "graph/ProjectGraph", diff --git a/packages/project/test/lib/ui5framework/AbstractResolver.js b/packages/project/test/lib/ui5framework/AbstractResolver.js index 7156df0a545..fe3065433d6 100644 --- a/packages/project/test/lib/ui5framework/AbstractResolver.js +++ b/packages/project/test/lib/ui5framework/AbstractResolver.js @@ -1,18 +1,19 @@ import test from "ava"; import sinon from "sinon"; import path from "node:path"; -import os from "node:os"; import esmock from "esmock"; test.beforeEach(async (t) => { - t.context.osHomeDirStub = sinon.stub().callsFake(() => os.homedir()); - t.context.AbstractResolver = await esmock.p("../../../lib/ui5Framework/AbstractResolver.js", { - "node:os": { - homedir: t.context.osHomeDirStub - } - }); + t.context.AbstractResolver = await esmock.p("../../../lib/ui5Framework/AbstractResolver.js", {}); class MyResolver extends t.context.AbstractResolver { + constructor(options = {}) { + super({ + ui5DataDir: "/ui5DataDir", + ...options + }); + } + static async fetchAllVersions() {} } @@ -118,24 +119,19 @@ test("AbstractResolver: Set relative 'ui5DataDir'", (t) => { t.is(resolver._ui5DataDir, path.resolve("./my-ui5DataDir"), "Should be resolved 'ui5DataDir'"); }); -test("AbstractResolver: 'ui5DataDir' overriden os.homedir()", (t) => { - const {MyResolver, osHomeDirStub} = t.context; - - osHomeDirStub.returns("./"); +test("AbstractResolver: constructor without 'ui5DataDir' should throw", (t) => { + const {AbstractResolver} = t.context; - const resolver = new MyResolver({ - version: "1.75.0" - }); - t.is(resolver._ui5DataDir, path.resolve("./.ui5"), "Should be resolved 'ui5DataDir'"); -}); + class MyResolverWithoutDefaults extends AbstractResolver { + static async fetchAllVersions() {} + } -test("AbstractResolver: Defaults 'ui5DataDir' to ~/.ui5", (t) => { - const {MyResolver} = t.context; - const resolver = new MyResolver({ - version: "1.75.0", - cwd: "/test-project/" - }); - t.is(resolver._ui5DataDir, path.join(os.homedir(), ".ui5"), "Should default to ~/.ui5"); + t.throws(() => { + new MyResolverWithoutDefaults({ + version: "1.75.0", + cwd: "/test-project/" + }); + }, {message: "MyResolverWithoutDefaults: Missing parameter \"ui5DataDir\""}); }); test("AbstractResolver: getLibraryMetadata should throw an Error when not implemented", async (t) => { diff --git a/packages/project/test/lib/ui5framework/Openui5Resolver.js b/packages/project/test/lib/ui5framework/Openui5Resolver.js index 34d756987da..278a4077d74 100644 --- a/packages/project/test/lib/ui5framework/Openui5Resolver.js +++ b/packages/project/test/lib/ui5framework/Openui5Resolver.js @@ -44,7 +44,8 @@ test.serial("Openui5Resolver: getLibraryMetadata", async (t) => { const resolver = new Openui5Resolver({ cwd: "/test-project/", - version: "1.75.0" + version: "1.75.0", + ui5DataDir: "/ui5DataDir" }); t.context.fetchPackageManifestStub @@ -104,7 +105,8 @@ test.serial("Openui5Resolver: handleLibrary", async (t) => { const resolver = new Openui5Resolver({ cwd: "/test-project/", - version: "1.75.0" + version: "1.75.0", + ui5DataDir: "/ui5DataDir" }); const getLibraryMetadataStub = sinon.stub(resolver, "getLibraryMetadata"); diff --git a/packages/project/test/lib/ui5framework/Sapui5MavenSnapshotResolver.js b/packages/project/test/lib/ui5framework/Sapui5MavenSnapshotResolver.js index ed9a4de6cd9..a88836ae2fd 100644 --- a/packages/project/test/lib/ui5framework/Sapui5MavenSnapshotResolver.js +++ b/packages/project/test/lib/ui5framework/Sapui5MavenSnapshotResolver.js @@ -63,7 +63,8 @@ test.serial( const resolver = new Sapui5MavenSnapshotResolver({ cwd: "/test-project/", - version: "1.75.0" + version: "1.75.0", + ui5DataDir: "/ui5DataDir" }); const expectedMetadata = { @@ -114,7 +115,8 @@ test.serial("Sapui5MavenSnapshotResolver: getLibraryMetadata throws", async (t) const resolver = new Sapui5MavenSnapshotResolver({ cwd: "/test-project/", - version: "1.75.0" + version: "1.75.0", + ui5DataDir: "/ui5DataDir" }); const loadDistMetadataStub = sinon.stub(resolver, "loadDistMetadata"); @@ -132,7 +134,8 @@ test.serial("Sapui5MavenSnapshotResolver: handleLibrary", async (t) => { const resolver = new Sapui5MavenSnapshotResolver({ cwd: "/test-project/", - version: "1.116.0-SNAPSHOT" + version: "1.116.0-SNAPSHOT", + ui5DataDir: "/ui5DataDir" }); const loadDistMetadataStub = sinon.stub(resolver, "loadDistMetadata"); @@ -186,7 +189,8 @@ test.serial("Sapui5MavenSnapshotResolver: handleLibrary - legacy version", async const resolver = new Sapui5MavenSnapshotResolver({ cwd: "/test-project/", - version: "1.75.0-SNAPSHOT" + version: "1.75.0-SNAPSHOT", + ui5DataDir: "/ui5DataDir" }); const loadDistMetadataStub = sinon.stub(resolver, "loadDistMetadata"); @@ -240,6 +244,7 @@ test.serial("Sapui5MavenSnapshotResolver: handleLibrary - sources requested", as const resolver = new Sapui5MavenSnapshotResolver({ cwd: "/test-project/", version: "1.116.0-SNAPSHOT", + ui5DataDir: "/ui5DataDir", sources: true }); @@ -294,6 +299,7 @@ test.serial("Sapui5MavenSnapshotResolver: handleLibrary - sources requested with const resolver = new Sapui5MavenSnapshotResolver({ cwd: "/test-project/", version: "1.75.0-SNAPSHOT", + ui5DataDir: "/ui5DataDir", sources: true }); @@ -347,7 +353,8 @@ test.serial("Sapui5MavenSnapshotResolver: handleLibrary throws", async (t) => { const resolver = new Sapui5MavenSnapshotResolver({ cwd: "/test-project/", - version: "1.75.0" + version: "1.75.0", + ui5DataDir: "/ui5DataDir" }); sinon.stub(resolver, "getLibraryMetadata").resolves({}); diff --git a/packages/project/test/lib/ui5framework/Sapui5Resolver.js b/packages/project/test/lib/ui5framework/Sapui5Resolver.js index 63288d356ed..99c5bad9c44 100644 --- a/packages/project/test/lib/ui5framework/Sapui5Resolver.js +++ b/packages/project/test/lib/ui5framework/Sapui5Resolver.js @@ -37,7 +37,8 @@ test.serial( const resolver = new Sapui5Resolver({ cwd: "/test-project/", - version: "1.75.0" + version: "1.75.0", + ui5DataDir: "/ui5DataDir" }); t.context.getTargetDirForPackageStub.callsFake(({pkgName, version}) => { @@ -88,7 +89,8 @@ test.serial("Sapui5Resolver: handleLibrary", async (t) => { const resolver = new Sapui5Resolver({ cwd: "/test-project/", - version: "1.75.0" + version: "1.75.0", + ui5DataDir: "/ui5DataDir" }); const loadDistMetadataStub = sinon.stub(resolver, "loadDistMetadata"); @@ -207,7 +209,8 @@ test.serial( const resolver = new Sapui5Resolver({ cwd: "/test-project/", - version: "1.77.7" + version: "1.77.7", + ui5DataDir: "/ui5DataDir" }); const openui5LibraryMetadata = { diff --git a/packages/project/test/lib/utils/dataDir.js b/packages/project/test/lib/utils/dataDir.js new file mode 100644 index 00000000000..6ecc3324d9c --- /dev/null +++ b/packages/project/test/lib/utils/dataDir.js @@ -0,0 +1,130 @@ +import test from "ava"; +import path from "node:path"; +import os from "node:os"; +import sinon from "sinon"; +import esmock from "esmock"; + +test.beforeEach(async (t) => { + t.context.originalUi5DataDirEnv = process.env.UI5_DATA_DIR; + delete process.env.UI5_DATA_DIR; + + t.context.configGetUi5DataDirStub = sinon.stub().returns(undefined); + t.context.ConfigurationStub = { + fromFile: sinon.stub().resolves({ + getUi5DataDir: t.context.configGetUi5DataDirStub + }) + }; + + const {resolveUi5DataDir} = await esmock("../../../lib/utils/dataDir.js", { + "../../../lib/config/Configuration.js": t.context.ConfigurationStub + }); + t.context.resolveUi5DataDir = resolveUi5DataDir; +}); + +test.afterEach.always((t) => { + if (typeof t.context.originalUi5DataDirEnv === "undefined") { + delete process.env.UI5_DATA_DIR; + } else { + process.env.UI5_DATA_DIR = t.context.originalUi5DataDirEnv; + } + sinon.restore(); +}); + +test.serial("resolveUi5DataDir: returns ~/.ui5 when nothing is configured", async (t) => { + const {resolveUi5DataDir} = t.context; + const result = await resolveUi5DataDir(); + t.is(result, path.resolve(os.homedir(), ".ui5")); + t.true(path.isAbsolute(result), "default path must always be absolute"); +}); + +test.serial("resolveUi5DataDir: default is absolute even when HOME is relative", async (t) => { + const {resolveUi5DataDir: resolveWithRelativeHome} = await esmock("../../../lib/utils/dataDir.js", { + "../../../lib/config/Configuration.js": t.context.ConfigurationStub, + "node:os": {...os, homedir: () => "./relative-home"} + }); + const result = await resolveWithRelativeHome(); + t.true(path.isAbsolute(result), "result must be absolute even when os.homedir() is relative"); + esmock.purge(resolveWithRelativeHome); +}); + +test.serial("resolveUi5DataDir: returns value from UI5_DATA_DIR env var (absolute)", async (t) => { + const {resolveUi5DataDir} = t.context; + process.env.UI5_DATA_DIR = path.resolve("custom", "data", "dir"); + const result = await resolveUi5DataDir(); + t.is(result, path.resolve("custom", "data", "dir")); + t.is(t.context.ConfigurationStub.fromFile.callCount, 0, "Configuration not read when env var is set"); +}); + +test.serial("resolveUi5DataDir: absolute UI5_DATA_DIR ignores projectRootPath", async (t) => { + const {resolveUi5DataDir} = t.context; + process.env.UI5_DATA_DIR = path.resolve("custom", "data", "dir"); + const result = await resolveUi5DataDir({projectRootPath: path.resolve("some", "project")}); + t.is(result, path.resolve("custom", "data", "dir"), + "absolute path is returned as-is regardless of projectRootPath"); +}); + +test.serial("resolveUi5DataDir: resolves relative UI5_DATA_DIR env var against projectRootPath", async (t) => { + const {resolveUi5DataDir} = t.context; + process.env.UI5_DATA_DIR = "relative/data"; + const result = await resolveUi5DataDir({projectRootPath: path.resolve("my", "project")}); + t.is(result, path.join(path.resolve("my", "project"), "relative/data")); +}); + +test.serial( + "resolveUi5DataDir: resolves relative UI5_DATA_DIR " + + "env var against process.cwd() when no projectRootPath", + async (t) => { + const {resolveUi5DataDir} = t.context; + process.env.UI5_DATA_DIR = "relative/data"; + const result = await resolveUi5DataDir(); + t.is(result, path.resolve("relative/data")); + }, +); + +test.serial("resolveUi5DataDir: returns value from Configuration (absolute)", async (t) => { + const {resolveUi5DataDir} = t.context; + t.context.configGetUi5DataDirStub.returns(path.resolve("config", "data", "dir")); + const result = await resolveUi5DataDir(); + t.is(result, path.resolve("config", "data", "dir")); +}); + +test.serial("resolveUi5DataDir: absolute Configuration value ignores projectRootPath", async (t) => { + const {resolveUi5DataDir} = t.context; + t.context.configGetUi5DataDirStub.returns(path.resolve("config", "data", "dir")); + const result = await resolveUi5DataDir({projectRootPath: path.resolve("some", "project")}); + t.is(result, path.resolve("config", "data", "dir"), + "absolute path is returned as-is regardless of projectRootPath"); +}); + +test.serial("resolveUi5DataDir: resolves relative Configuration value against projectRootPath", async (t) => { + const {resolveUi5DataDir} = t.context; + t.context.configGetUi5DataDirStub.returns("my-data"); + const result = await resolveUi5DataDir({projectRootPath: path.resolve("my", "project")}); + t.is(result, path.join(path.resolve("my", "project"), "my-data")); +}); + +test.serial( + "resolveUi5DataDir: resolves relative Configuration value against" + + " process.cwd() when no projectRootPath", + async (t) => { + const {resolveUi5DataDir} = t.context; + t.context.configGetUi5DataDirStub.returns("my-data"); + const result = await resolveUi5DataDir(); + t.is(result, path.resolve("my-data")); + }, +); + +test.serial("resolveUi5DataDir: env var takes precedence over Configuration", async (t) => { + const {resolveUi5DataDir} = t.context; + process.env.UI5_DATA_DIR = path.resolve("custom", "data", "dir"); + t.context.configGetUi5DataDirStub.returns(path.resolve("config", "data", "dir")); + const result = await resolveUi5DataDir(); + t.is(result, path.resolve("custom", "data", "dir")); +}); + +test.serial("resolveUi5DataDir: uses process.cwd() when projectRootPath is not provided", async (t) => { + const {resolveUi5DataDir} = t.context; + t.context.configGetUi5DataDirStub.returns("relative/data"); + const result = await resolveUi5DataDir(); + t.is(result, path.resolve(process.cwd(), "relative/data")); +});