UNPKG

@ui5/project

Version:
276 lines (249 loc) 9.49 kB
import path from "node:path"; import os from "node:os"; import semver from "semver"; import AbstractResolver from "./AbstractResolver.js"; import Installer from "./maven/Installer.js"; import {getLogger} from "@ui5/logger"; const log = getLogger("ui5Framework:Sapui5MavenSnapshotResolver"); const DIST_PKG_NAME = "@sapui5/distribution-metadata"; const DIST_GROUP_ID = "com.sap.ui5.dist"; const DIST_ARTIFACT_ID = "sapui5-sdk-dist"; /** * Resolver for the SAPUI5 framework * * This Resolver downloads and installs SNAPSHOTS of UI5 libraries from * a Maven repository. It's meant for internal usage only as no use cases * outside of SAP are known. * * @public * @class * @alias @ui5/project/ui5Framework/Sapui5MavenSnapshotResolver * @extends @ui5/project/ui5Framework/AbstractResolver */ class Sapui5MavenSnapshotResolver extends AbstractResolver { /** * @param {*} options options * @param {string} [options.snapshotEndpointUrl] Maven Repository Snapshot URL. Can by overruled * by setting the <code>UI5_MAVEN_SNAPSHOT_ENDPOINT_URL</code> environment variable. If neither is provided, * falling back to the standard Maven settings.xml file (if existing). * @param {string} options.version SAPUI5 version to use * @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 {module:@ui5/project/ui5Framework/maven/CacheMode} [options.cacheMode=Default] * Cache mode to use */ constructor(options) { super(options); const { cacheMode, } = options; this._installer = new Installer({ ui5DataDir: this._ui5DataDir, snapshotEndpointUrlCb: Sapui5MavenSnapshotResolver._createSnapshotEndpointUrlCallback(options.snapshotEndpointUrl), cacheMode, }); this._loadDistMetadata = null; // TODO 5.0: Remove support for legacy snapshot versions this._isLegacySnapshotVersion = semver.lt(this._version, "1.116.0-SNAPSHOT", { includePrerelease: true }); } loadDistMetadata() { if (!this._loadDistMetadata) { this._loadDistMetadata = Promise.resolve().then(async () => { const version = this._version; log.verbose( `Installing ${DIST_ARTIFACT_ID} in version ${version}...` ); const {pkgPath: distPkgPath} = await this._installer.installPackage({ pkgName: DIST_PKG_NAME, groupId: DIST_GROUP_ID, artifactId: DIST_ARTIFACT_ID, version, classifier: "npm-sources", extension: "zip", }); return await this._installer.readJson( path.join(distPkgPath, "metadata.json") ); }); } return this._loadDistMetadata; } async getLibraryMetadata(libraryName) { const distMetadata = await this.loadDistMetadata(); const metadata = distMetadata.libraries[libraryName]; if (!metadata) { throw new Error(`Could not find library "${libraryName}"`); } return metadata; } async handleLibrary(libraryName) { const metadata = await this.getLibraryMetadata(libraryName); if (!metadata.gav) { throw new Error( "Metadata is missing GAV (group, artifact and version) " + "information. This might indicate an unsupported SNAPSHOT version." ); } const gav = metadata.gav.split(":"); let pkgName = metadata.npmPackageName; // Use "npm-dist" artifact by default let classifier; let extension; if (this._sources) { // Use npm-sources artifact if sources are requested classifier = "npm-sources"; extension = "zip"; } else { // Add "prebuilt" suffix to package name pkgName += "-prebuilt"; if (this._isLegacySnapshotVersion) { // For legacy versions < 1.116.0-SNAPSHOT where npm-dist artifact is not // yet available, use "default" JAR classifier = null; extension = "jar"; } else { // Use "npm-dist" artifact by default classifier = "npm-dist"; extension = "zip"; } } return { metadata: Promise.resolve({ id: pkgName, version: metadata.version, dependencies: metadata.dependencies, optionalDependencies: metadata.optionalDependencies, }), // Trigger installation of package install: this._installer.installPackage({ pkgName, groupId: gav[0], artifactId: gav[1], version: metadata.version, classifier, extension, }), }; } static async fetchAllVersions({ui5DataDir, cwd, snapshotEndpointUrl} = {}) { const installer = new Installer({ cwd: cwd ? path.resolve(cwd) : process.cwd(), ui5DataDir: path.resolve( ui5DataDir || path.join(os.homedir(), ".ui5") ), snapshotEndpointUrlCb: Sapui5MavenSnapshotResolver._createSnapshotEndpointUrlCallback(snapshotEndpointUrl), }); return await installer.fetchPackageVersions({ groupId: DIST_GROUP_ID, artifactId: DIST_ARTIFACT_ID, }); } static _createSnapshotEndpointUrlCallback(snapshotEndpointUrl) { snapshotEndpointUrl = process.env.UI5_MAVEN_SNAPSHOT_ENDPOINT_URL || snapshotEndpointUrl; if (!snapshotEndpointUrl) { // Here we return a function which returns a promise that resolves with the URL. // If we would already start resolving the settings.xml at this point, we'd need to always ask the // end user for confirmation whether the resolved URL should be used. In some cases where the resources // are already cached, this is actually not necessary and could be skipped return Sapui5MavenSnapshotResolver._resolveSnapshotEndpointUrl; } else { return () => Promise.resolve(snapshotEndpointUrl); } } /** * Read the Maven repository snapshot endpoint URL from the central * UI5 Tooling configuration, with a fallback to central Maven configuration (is existing) * * @returns {Promise<string>} The resolved snapshotEndpointUrl */ static async _resolveSnapshotEndpointUrl() { const {default: Configuration} = await import("../config/Configuration.js"); const config = await Configuration.fromFile(); let url = config.getMavenSnapshotEndpointUrl(); if (url) { log.verbose(`Using UI5 Tooling configuration for mavenSnapshotEndpointUrl: ${url}`); } else { log.verbose(`No mavenSnapshotEndpointUrl configuration found`); url = await Sapui5MavenSnapshotResolver._resolveSnapshotEndpointUrlFromMaven(); if (url) { log.verbose(`Updating UI5 Tooling configuration with new mavenSnapshotEndpointUrl: ${url}`); const configJson = config.toJson(); configJson.mavenSnapshotEndpointUrl = url; await Configuration.toFile(new Configuration(configJson)); } } return url; } /** * Tries to detect whether ~/.m2/settings.xml exist, and if so, whether * the snapshot.build URL is extracted from there * * @param {string} [settingsXML=~/.m2/settings.xml] Path to the settings.xml. * If not provided, the default location is used * @returns {Promise<string>} The resolved snapshot.build URL from ~/.m2/settings.xml */ static async _resolveSnapshotEndpointUrlFromMaven(settingsXML) { if (!process.stdout.isTTY) { // We can't prompt the user if stdout is non-interactive (i.e. in CI environments) // Therefore skip resolution from Maven settings.xml altogether return null; } settingsXML = settingsXML || path.resolve(path.join(os.homedir(), ".m2", "settings.xml")); const {default: fs} = await import("graceful-fs"); const {promisify} = await import("node:util"); const readFile = promisify(fs.readFile); const xml2js = await import("xml2js"); const parser = new xml2js.Parser({ preserveChildrenOrder: true, xmlns: true, }); let url; log.verbose(`Attempting to resolve snapshot endpoint URL from Maven configuration file at ${settingsXML}...`); try { const fileContent = await readFile(settingsXML); const xmlContents = await parser.parseStringPromise(fileContent); const snapshotBuildChunk = xmlContents?.settings?.profiles[0]?.profile.filter( (prof) => prof.id[0]._ === "snapshot.build" )[0]; url = snapshotBuildChunk?.repositories?.[0]?.repository?.[0]?.url?.[0]?._ || snapshotBuildChunk?.pluginRepositories?.[0]?.pluginRepository?.[0]?.url?.[0]?._; if (!url) { log.verbose(`"snapshot.build" attribute could not be found in ${settingsXML}`); return null; } } catch (err) { if (err.code === "ENOENT") { // "File or directory does not exist" log.verbose(`File does not exist: ${settingsXML}`); } else { log.warning(`Failed to read Maven configuration file from ${settingsXML}: ${err.message}`); } return null; } const {default: yesno} = await import("yesno"); const ok = await yesno({ question: "\nA Maven repository endpoint URL is required for consuming snapshot versions of UI5 libraries.\n" + "You can configure one using the command: 'ui5 config set mavenSnapshotEndpointUrl <url>'\n\n" + `The following URL has been found in a Maven configuration file at ${settingsXML}:\n${url}\n\n` + `Continue with this endpoint URL and remember it for the future? (yes)`, defaultValue: true, }); if (ok) { log.verbose(`Using Maven snapshot endpoint URL resolved from Maven configuration file: ${url}`); return url; } else { log.verbose(`User rejected usage of the resolved URL`); return null; } } } export default Sapui5MavenSnapshotResolver;