UNPKG

@ui5/project

Version:
433 lines (388 loc) 16.4 kB
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"; class ProjectProcessor { constructor({libraryMetadata, graph, workspace}) { this._libraryMetadata = libraryMetadata; this._graph = graph; this._workspace = workspace; this._projectGraphPromises = Object.create(null); } async addProjectToGraph(libName, ancestors) { if (ancestors) { this._checkCycle(ancestors, libName); } if (this._projectGraphPromises[libName]) { return this._projectGraphPromises[libName]; } return this._projectGraphPromises[libName] = this._addProjectToGraph(libName, ancestors); } async _addProjectToGraph(libName, ancestors = []) { log.verbose(`Creating project for library ${libName}...`); if (!this._libraryMetadata[libName]) { throw new Error(`Failed to find library ${libName} in dist packages metadata.json`); } const depMetadata = this._libraryMetadata[libName]; const graph = this._graph; if (graph.getProject(libName)) { // Already added return; } const dependencies = await Promise.all(depMetadata.dependencies.map(async (depName) => { await this.addProjectToGraph(depName, [...ancestors, libName]); return depName; })); if (depMetadata.optionalDependencies) { const resolvedOptionals = await Promise.all(depMetadata.optionalDependencies.map(async (depName) => { if (this._libraryMetadata[depName]) { log.verbose(`Resolving optional dependency ${depName} for project ${libName}...`); await this.addProjectToGraph(depName, [...ancestors, libName]); return depName; } })); dependencies.push(...resolvedOptionals.filter(($)=>$)); } let projectIsFromWorkspace = false; let ui5Module; if (this._workspace) { ui5Module = await this._workspace.getModuleByProjectName(libName); if (ui5Module) { log.info(`Resolved project ${libName} via ${this._workspace.getName()} workspace ` + `to version ${ui5Module.getVersion()}`); log.verbose(` Resolved module ${libName} to path ${ui5Module.getPath()}`); log.verbose(` Requested version was: ${depMetadata.version}`); projectIsFromWorkspace = true; } } if (!ui5Module) { ui5Module = new Module({ id: depMetadata.id, version: depMetadata.version, modulePath: depMetadata.path }); } const {project} = await ui5Module.getSpecifications(); graph.addProject(project); dependencies.forEach((dependency) => { graph.declareDependency(libName, dependency); }); if (projectIsFromWorkspace) { // Add any dependencies that are only declared in the workspace resolved project // Do not remove superfluous dependencies (might be added later though) await Promise.all(project.getFrameworkDependencies().map(async ({name, optional, development}) => { // Only proceed with dependencies which are not "optional" or "development", // and not already listed in the dependencies of the original node if (optional || development || dependencies.includes(name)) { return; } if (!this._libraryMetadata[name]) { throw new Error( `Unable to find dependency ${name}, required by project ${project.getName()} ` + `(resolved via ${this._workspace.getName()} workspace) in current set of libraries. ` + `Try adding it temporarily to the root project's dependencies`); } await this.addProjectToGraph(name, [...ancestors, libName]); graph.declareDependency(libName, name); })); } } _checkCycle(ancestors, projectName) { if (ancestors.includes(projectName)) { // "Back-edge" detected. This would cause a deadlock // Mark first and last occurrence in chain with an asterisk and throw an error detailing the // problematic dependency chain ancestors[ancestors.indexOf(projectName)] = `*${projectName}*`; throw new Error( `ui5Framework:ProjectPreprocessor: Detected cyclic dependency chain: ` + `${ancestors.join(" -> ")} -> *${projectName}*`); } } } const utils = { shouldIncludeDependency({optional, development}, root) { // Root project should include all dependencies // Otherwise only non-optional and non-development dependencies should be included return root || (optional !== true && development !== true); }, async getFrameworkLibrariesFromGraph(projectGraph) { const ui5Dependencies = []; const rootProject = projectGraph.getRoot(); await projectGraph.traverseBreadthFirst(async ({project}) => { if (project !== rootProject && project.isFrameworkProject()) { // Ignoring UI5 Framework libraries in dependencies return; } // No need to check for specVersion since Specification API is >= 2.0 anyways const frameworkDependencies = project.getFrameworkDependencies(); if (!frameworkDependencies.length) { log.verbose(`Project ${project.getName()} has no framework dependencies`); // Possible future enhancement: Fallback to detect OpenUI5 framework dependencies in package.json return; } frameworkDependencies.forEach((dependency) => { if (!ui5Dependencies.includes(dependency.name) && utils.shouldIncludeDependency(dependency, project === rootProject)) { ui5Dependencies.push(dependency.name); } }); }); return ui5Dependencies; }, async declareFrameworkDependenciesInGraph(projectGraph) { const rootProject = projectGraph.getRoot(); await projectGraph.traverseBreadthFirst(async ({project}) => { if (project !== rootProject && project.isFrameworkProject()) { // Ignoring UI5 Framework libraries in dependencies return; } // No need to check for specVersion since Specification API is >= 2.0 anyways const frameworkDependencies = project.getFrameworkDependencies(); if (!frameworkDependencies.length) { log.verbose(`Project ${project.getName()} has no framework dependencies`); // Possible future enhancement: Fallback to detect OpenUI5 framework dependencies in package.json return; } const isRoot = project === rootProject; frameworkDependencies.forEach((dependency) => { if (isRoot || !dependency.development) { // Root project should include all dependencies // Otherwise all non-development dependencies should be considered if (isRoot) { // Check for deprecated/internal dependencies of the root project const depProject = projectGraph.getProject(dependency.name); if (depProject && depProject.isDeprecated() && rootProject.getName() !== "testsuite") { // No warning for testsuite projects log.warn(`Dependency ${depProject.getName()} is deprecated ` + `and should not be used for new projects!`); } if (depProject && depProject.isSapInternal() && !rootProject.getAllowSapInternal()) { // Do not warn if project defines "allowSapInternal" log.warn(`Dependency ${depProject.getName()} is restricted for use by ` + `SAP internal projects only! ` + `If the project ${rootProject.getName()} is an SAP internal project, ` + `add the attribute "allowSapInternal: true" to its metadata configuration`); } } if (!isRoot && dependency.optional) { if (projectGraph.getProject(dependency.name)) { projectGraph.declareOptionalDependency(project.getName(), dependency.name); } } else { projectGraph.declareDependency(project.getName(), dependency.name); } } }); }); await projectGraph.resolveOptionalDependencies(); }, checkForDuplicateFrameworkProjects(projectGraph, frameworkGraph) { // Check for duplicate framework libraries const projectGraphProjectNames = projectGraph.getProjectNames(); const duplicateFrameworkProjectNames = frameworkGraph.getProjectNames() .filter((name) => projectGraphProjectNames.includes(name)); if (duplicateFrameworkProjectNames.length) { throw new Error( `Duplicate framework dependency definition(s) found for project ${projectGraph.getRoot().getName()}: ` + `${duplicateFrameworkProjectNames.join(", ")}.\n` + `Framework libraries should only be referenced via ui5.yaml configuration. ` + `Neither the root project, nor any of its dependencies should include them as direct ` + `dependencies (e.g. via package.json).` ); } }, /** * 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 }; /** * * * @private * @module @ui5/project/helpers/ui5Framework */ export default { /** * * * @public * @param {@ui5/project/graph/ProjectGraph} projectGraph * @param {object} [options] * @param {string} [options.versionOverride] Framework version to use instead of the root projects framework * version * @param {module:@ui5/project/ui5Framework/maven/CacheMode} [options.cacheMode] * Cache mode to use when consuming SNAPSHOT versions of a framework * @param {@ui5/project/graph/Workspace} [options.workspace] * Optional workspace instance to use for overriding node resolutions * @returns {Promise<@ui5/project/graph/ProjectGraph>} * Promise resolving with the given graph instance to allow method chaining */ enrichProjectGraph: async function(projectGraph, options = {}) { const {workspace, cacheMode} = options; const rootProject = projectGraph.getRoot(); const frameworkName = rootProject.getFrameworkName(); const frameworkVersion = rootProject.getFrameworkVersion(); const cwd = rootProject.getRootPath(); // 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 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); }); // 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; } } if (!frameworkName && !frameworkVersion) { log.verbose(`Root project ${rootProject.getName()} has no framework configuration. Nothing to do here`); return projectGraph; } if (frameworkName !== "SAPUI5" && frameworkName !== "OpenUI5") { throw new Error( `Unknown framework.name "${frameworkName}" for project ${rootProject.getName()}. ` + `Must be "OpenUI5" or "SAPUI5"` ); } const referencedLibraries = await utils.getFrameworkLibrariesFromGraph(projectGraph); if (!referencedLibraries.length) { log.verbose( `No ${frameworkName} libraries referenced in project ${rootProject.getName()} ` + `or in any of its dependencies`); return projectGraph; } let Resolver; if (version && version.toLowerCase().endsWith("-snapshot")) { Resolver = (await import("../../ui5Framework/Sapui5MavenSnapshotResolver.js")).default; } else if (frameworkName === "OpenUI5") { Resolver = (await import("../../ui5Framework/Openui5Resolver.js")).default; } else if (frameworkName === "SAPUI5") { 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); } if (options.versionOverride) { version = await Resolver.resolveVersion(options.versionOverride, { ui5DataDir, cwd }); log.info( `Overriding configured ${frameworkName} version ` + `${frameworkVersion} with version ${version}` ); } if (version) { log.info(`Using ${frameworkName} version: ${version}`); } let providedLibraryMetadata; if (workspace) { providedLibraryMetadata = await utils.getWorkspaceFrameworkLibraryMetadata({ workspace, projectGraph }); } // 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, version, providedLibraryMetadata, cacheMode, ui5DataDir }); let startTime; if (log.isLevelEnabled("verbose")) { startTime = process.hrtime(); } const {libraryMetadata} = await resolver.install(referencedLibraries); if (log.isLevelEnabled("verbose")) { const timeDiff = process.hrtime(startTime); const {default: prettyHrtime} = await import("pretty-hrtime"); log.verbose( `${frameworkName} dependencies ${referencedLibraries.join(", ")} ` + `resolved in ${prettyHrtime(timeDiff)}`); } const frameworkGraph = new ProjectGraph({ rootProjectName: `fake-root-of-${rootProject.getName()}-framework-dependency-graph` }); const projectProcessor = new utils.ProjectProcessor({ libraryMetadata, graph: frameworkGraph, workspace }); await Promise.all(referencedLibraries.map(async (libName) => { await projectProcessor.addProjectToGraph(libName); })); utils.checkForDuplicateFrameworkProjects(projectGraph, frameworkGraph); log.verbose("Joining framework graph into project graph..."); projectGraph.join(frameworkGraph); await utils.declareFrameworkDependenciesInGraph(projectGraph); return projectGraph; }, // Export for testing only _utils: process.env.NODE_ENV === "test" ? utils : /* istanbul ignore next */ undefined };