project/lib/ui5Framework/Sapui5MavenSnapshotResolver.js

  1. import path from "node:path";
  2. import os from "node:os";
  3. import semver from "semver";
  4. import AbstractResolver from "./AbstractResolver.js";
  5. import Installer from "./maven/Installer.js";
  6. import {getLogger} from "@ui5/logger";
  7. const log = getLogger("ui5Framework:Sapui5MavenSnapshotResolver");
  8. const DIST_PKG_NAME = "@sapui5/distribution-metadata";
  9. const DIST_GROUP_ID = "com.sap.ui5.dist";
  10. const DIST_ARTIFACT_ID = "sapui5-sdk-dist";
  11. /**
  12. * Resolver for the SAPUI5 framework
  13. *
  14. * This Resolver downloads and installs SNAPSHOTS of UI5 libraries from
  15. * a Maven repository. It's meant for internal usage only as no use cases
  16. * outside of SAP are known.
  17. *
  18. * @public
  19. * @class
  20. * @alias @ui5/project/ui5Framework/Sapui5MavenSnapshotResolver
  21. * @extends @ui5/project/ui5Framework/AbstractResolver
  22. */
  23. class Sapui5MavenSnapshotResolver extends AbstractResolver {
  24. /**
  25. * @param {*} options options
  26. * @param {string} [options.snapshotEndpointUrl] Maven Repository Snapshot URL. Can by overruled
  27. * by setting the <code>UI5_MAVEN_SNAPSHOT_ENDPOINT_URL</code> environment variable. If neither is provided,
  28. * falling back to the standard Maven settings.xml file (if existing).
  29. * @param {string} options.version SAPUI5 version to use
  30. * @param {boolean} [options.sources=false] Whether to install framework libraries as sources or
  31. * pre-built (with build manifest)
  32. * @param {string} [options.cwd=process.cwd()] Current working directory
  33. * @param {string} [options.ui5HomeDir="~/.ui5"] UI5 home directory location. This will be used to store packages,
  34. * metadata and configuration used by the resolvers. Relative to `process.cwd()`
  35. * @param {module:@ui5/project/ui5Framework/maven/CacheMode} [options.cacheMode=Default]
  36. * Cache mode to use
  37. */
  38. constructor(options) {
  39. super(options);
  40. const {
  41. cacheMode,
  42. } = options;
  43. this._installer = new Installer({
  44. ui5HomeDir: this._ui5HomeDir,
  45. snapshotEndpointUrlCb:
  46. Sapui5MavenSnapshotResolver._createSnapshotEndpointUrlCallback(options.snapshotEndpointUrl),
  47. cacheMode,
  48. });
  49. this._loadDistMetadata = null;
  50. // TODO 4.0: Remove support for legacy snapshot versions
  51. this._isLegacySnapshotVersion = semver.lt(this._version, "1.116.0-SNAPSHOT", {
  52. includePrerelease: true
  53. });
  54. }
  55. loadDistMetadata() {
  56. if (!this._loadDistMetadata) {
  57. this._loadDistMetadata = Promise.resolve().then(async () => {
  58. const version = this._version;
  59. log.verbose(
  60. `Installing ${DIST_ARTIFACT_ID} in version ${version}...`
  61. );
  62. const {pkgPath: distPkgPath} = await this._installer.installPackage({
  63. pkgName: DIST_PKG_NAME,
  64. groupId: DIST_GROUP_ID,
  65. artifactId: DIST_ARTIFACT_ID,
  66. version,
  67. classifier: "npm-sources",
  68. extension: "zip",
  69. });
  70. return await this._installer.readJson(
  71. path.join(distPkgPath, "metadata.json")
  72. );
  73. });
  74. }
  75. return this._loadDistMetadata;
  76. }
  77. async getLibraryMetadata(libraryName) {
  78. const distMetadata = await this.loadDistMetadata();
  79. const metadata = distMetadata.libraries[libraryName];
  80. if (!metadata) {
  81. throw new Error(`Could not find library "${libraryName}"`);
  82. }
  83. return metadata;
  84. }
  85. async handleLibrary(libraryName) {
  86. const metadata = await this.getLibraryMetadata(libraryName);
  87. if (!metadata.gav) {
  88. throw new Error(
  89. "Metadata is missing GAV (group, artifact and version) " +
  90. "information. This might indicate an unsupported SNAPSHOT version."
  91. );
  92. }
  93. const gav = metadata.gav.split(":");
  94. let pkgName = metadata.npmPackageName;
  95. // Use "npm-dist" artifact by default
  96. let classifier;
  97. let extension;
  98. if (this._sources) {
  99. // Use npm-sources artifact if sources are requested
  100. classifier = "npm-sources";
  101. extension = "zip";
  102. } else {
  103. // Add "prebuilt" suffix to package name
  104. pkgName += "-prebuilt";
  105. if (this._isLegacySnapshotVersion) {
  106. // For legacy versions < 1.116.0-SNAPSHOT where npm-dist artifact is not
  107. // yet available, use "default" JAR
  108. classifier = null;
  109. extension = "jar";
  110. } else {
  111. // Use "npm-dist" artifact by default
  112. classifier = "npm-dist";
  113. extension = "zip";
  114. }
  115. }
  116. return {
  117. metadata: Promise.resolve({
  118. id: pkgName,
  119. version: metadata.version,
  120. dependencies: metadata.dependencies,
  121. optionalDependencies: metadata.optionalDependencies,
  122. }),
  123. // Trigger installation of package
  124. install: this._installer.installPackage({
  125. pkgName,
  126. groupId: gav[0],
  127. artifactId: gav[1],
  128. version: metadata.version,
  129. classifier,
  130. extension,
  131. }),
  132. };
  133. }
  134. static async fetchAllVersions({ui5HomeDir, cwd, snapshotEndpointUrl} = {}) {
  135. const installer = new Installer({
  136. cwd: cwd ? path.resolve(cwd) : process.cwd(),
  137. ui5HomeDir: path.resolve(
  138. ui5HomeDir || path.join(os.homedir(), ".ui5")
  139. ),
  140. snapshotEndpointUrlCb: Sapui5MavenSnapshotResolver._createSnapshotEndpointUrlCallback(snapshotEndpointUrl),
  141. });
  142. return await installer.fetchPackageVersions({
  143. groupId: DIST_GROUP_ID,
  144. artifactId: DIST_ARTIFACT_ID,
  145. });
  146. }
  147. static _createSnapshotEndpointUrlCallback(snapshotEndpointUrl) {
  148. snapshotEndpointUrl = process.env.UI5_MAVEN_SNAPSHOT_ENDPOINT_URL || snapshotEndpointUrl;
  149. if (!snapshotEndpointUrl) {
  150. // Here we return a function which returns a promise that resolves with the URL.
  151. // If we would already start resolving the settings.xml at this point, we'd need to always ask the
  152. // end user for confirmation whether the resolved URL should be used. In some cases where the resources
  153. // are already cached, this is actually not necessary and could be skipped
  154. return Sapui5MavenSnapshotResolver._resolveSnapshotEndpointUrl;
  155. } else {
  156. return () => Promise.resolve(snapshotEndpointUrl);
  157. }
  158. }
  159. /**
  160. * Read the Maven repository snapshot endpoint URL from the central
  161. * UI5 Tooling configuration, with a fallback to central Maven configuration (is existing)
  162. *
  163. * @returns {Promise<string>} The resolved snapshotEndpointUrl
  164. */
  165. static async _resolveSnapshotEndpointUrl() {
  166. const {default: Configuration} = await import("../config/Configuration.js");
  167. const config = await Configuration.fromFile();
  168. let url = config.getMavenSnapshotEndpointUrl();
  169. if (url) {
  170. log.verbose(`Using UI5 Tooling configuration for mavenSnapshotEndpointUrl: ${url}`);
  171. } else {
  172. log.verbose(`No mavenSnapshotEndpointUrl configuration found`);
  173. url = await Sapui5MavenSnapshotResolver._resolveSnapshotEndpointUrlFromMaven();
  174. if (url) {
  175. log.verbose(`Updating UI5 Tooling configuration with new mavenSnapshotEndpointUrl: ${url}`);
  176. const configJson = config.toJson();
  177. configJson.mavenSnapshotEndpointUrl = url;
  178. await Configuration.toFile(new Configuration(configJson));
  179. }
  180. }
  181. return url;
  182. }
  183. /**
  184. * Tries to detect whether ~/.m2/settings.xml exist, and if so, whether
  185. * the snapshot.build URL is extracted from there
  186. *
  187. * @param {string} [settingsXML=~/.m2/settings.xml] Path to the settings.xml.
  188. * If not provided, the default location is used
  189. * @returns {Promise<string>} The resolved snapshot.build URL from ~/.m2/settings.xml
  190. */
  191. static async _resolveSnapshotEndpointUrlFromMaven(settingsXML) {
  192. if (!process.stdout.isTTY) {
  193. // We can't prompt the user if stdout is non-interactive (i.e. in CI environments)
  194. // Therefore skip resolution from Maven settings.xml altogether
  195. return null;
  196. }
  197. settingsXML =
  198. settingsXML || path.resolve(path.join(os.homedir(), ".m2", "settings.xml"));
  199. const {default: fs} = await import("graceful-fs");
  200. const {promisify} = await import("node:util");
  201. const readFile = promisify(fs.readFile);
  202. const xml2js = await import("xml2js");
  203. const parser = new xml2js.Parser({
  204. preserveChildrenOrder: true,
  205. xmlns: true,
  206. });
  207. let url;
  208. log.verbose(`Attempting to resolve snapshot endpoint URL from Maven configuration file at ${settingsXML}...`);
  209. try {
  210. const fileContent = await readFile(settingsXML);
  211. const xmlContents = await parser.parseStringPromise(fileContent);
  212. const snapshotBuildChunk = xmlContents?.settings?.profiles[0]?.profile.filter(
  213. (prof) => prof.id[0]._ === "snapshot.build"
  214. )[0];
  215. url =
  216. snapshotBuildChunk?.repositories?.[0]?.repository?.[0]?.url?.[0]?._ ||
  217. snapshotBuildChunk?.pluginRepositories?.[0]?.pluginRepository?.[0]?.url?.[0]?._;
  218. if (!url) {
  219. log.verbose(`"snapshot.build" attribute could not be found in ${settingsXML}`);
  220. return null;
  221. }
  222. } catch (err) {
  223. if (err.code === "ENOENT") {
  224. // "File or directory does not exist"
  225. log.verbose(`File does not exist: ${settingsXML}`);
  226. } else {
  227. log.warning(`Failed to read Maven configuration file from ${settingsXML}: ${err.message}`);
  228. }
  229. return null;
  230. }
  231. const {default: yesno} = await import("yesno");
  232. const ok = await yesno({
  233. question:
  234. "\nA Maven repository endpoint URL is required for consuming snapshot versions of UI5 libraries.\n" +
  235. "You can configure one using the command: 'ui5 config set mavenSnapshotEndpointUrl <url>'\n\n" +
  236. `The following URL has been found in a Maven configuration file at ${settingsXML}:\n${url}\n\n` +
  237. `Continue with this endpoint URL and remember it for the future? (yes)`,
  238. defaultValue: true,
  239. });
  240. if (ok) {
  241. log.verbose(`Using Maven snapshot endpoint URL resolved from Maven configuration file: ${url}`);
  242. return url;
  243. } else {
  244. log.verbose(`User rejected usage of the resolved URL`);
  245. return null;
  246. }
  247. }
  248. }
  249. export default Sapui5MavenSnapshotResolver;