project/lib/ui5Framework/AbstractResolver.js

  1. const path = require("path");
  2. const log = require("@ui5/logger").getLogger("ui5Framework:AbstractResolver");
  3. const semver = require("semver");
  4. // Matches Semantic Versioning 2.0.0 versions
  5. // https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
  6. //
  7. // This needs to be aligned with the ui5.yaml JSON schema:
  8. // lib/validation/schema/specVersion/2.0/kind/project.json#/definitions/framework/properties/version/pattern
  9. //
  10. // eslint-disable-next-line max-len
  11. const SEMVER_VERSION_REGEXP = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
  12. // Reduced Semantic Versioning pattern
  13. // Matches MAJOR.MINOR as a simple version range to be resolved to the latest patch
  14. const VERSION_RANGE_REGEXP = /^(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
  15. /**
  16. * Abstract Resolver
  17. *
  18. * @public
  19. * @abstract
  20. * @memberof module:@ui5/project.ui5Framework
  21. */
  22. class AbstractResolver {
  23. /**
  24. * @param {*} options options
  25. * @param {string} options.version Framework version to use
  26. * @param {string} [options.cwd=process.cwd()] Working directory to resolve configurations like .npmrc
  27. * @param {string} [options.ui5HomeDir="~/.ui5"] UI5 home directory location. This will be used to store packages,
  28. * metadata and configuration used by the resolvers. Relative to `process.cwd()`
  29. */
  30. constructor({cwd, version, ui5HomeDir}) {
  31. if (new.target === AbstractResolver) {
  32. throw new TypeError("Class 'AbstractResolver' is abstract");
  33. }
  34. if (!version) {
  35. throw new Error(`AbstractResolver: Missing parameter "version"`);
  36. }
  37. this._ui5HomeDir = ui5HomeDir ? path.resolve(ui5HomeDir) : path.join(require("os").homedir(), ".ui5");
  38. this._cwd = cwd ? path.resolve(cwd) : process.cwd();
  39. this._version = version;
  40. }
  41. async _processLibrary(libraryName, libraryMetadata, errors) {
  42. // Check if library is already processed
  43. if (libraryMetadata[libraryName]) {
  44. return;
  45. }
  46. // Mark library as handled
  47. libraryMetadata[libraryName] = {};
  48. log.verbose("Processing " + libraryName);
  49. const promises = await this.handleLibrary(libraryName);
  50. const [metadata, {pkgPath}] = await Promise.all([
  51. promises.metadata.then((metadata) =>
  52. this._processDependencies(libraryName, metadata, libraryMetadata, errors)),
  53. promises.install
  54. ]);
  55. // Add path to installed package to metadata
  56. metadata.path = pkgPath;
  57. // Add metadata entry
  58. libraryMetadata[libraryName] = metadata;
  59. }
  60. async _processDependencies(libraryName, metadata, libraryMetadata, errors) {
  61. if (metadata.dependencies.length > 0) {
  62. log.verbose("Processing dependencies of " + libraryName);
  63. await this._processLibraries(metadata.dependencies, libraryMetadata, errors);
  64. log.verbose("Done processing dependencies of " + libraryName);
  65. }
  66. return metadata;
  67. }
  68. async _processLibraries(libraryNames, libraryMetadata, errors) {
  69. const results = await Promise.all(libraryNames.map(async (libraryName) => {
  70. try {
  71. await this._processLibrary(libraryName, libraryMetadata, errors);
  72. } catch (err) {
  73. log.verbose(`Failed to process library ${libraryName}`);
  74. log.verbose(`Error: ${err.message}`);
  75. log.verbose(`Call stack: ${err.stack}`);
  76. return `Failed to resolve library ${libraryName}: ${err.message}`;
  77. }
  78. }));
  79. // Don't add empty results (success)
  80. errors.push(...results.filter(($) => $));
  81. }
  82. /**
  83. * Library metadata entry
  84. *
  85. * @example
  86. * const libraryMetadataEntry = {
  87. * "id": "@openui5/sap.ui.core",
  88. * "version": "1.75.0",
  89. * "path": "~/.ui5/framework/packages/@openui5/sap.ui.core/1.75.0",
  90. * "dependencies": [],
  91. * "optionalDependencies": []
  92. * };
  93. *
  94. * @public
  95. * @typedef {object} LibraryMetadataEntry
  96. * @property {string} id Identifier
  97. * @property {string} version Version
  98. * @property {string} path Path
  99. * @property {string[]} dependencies List of dependency ids
  100. * @property {string[]} optionalDependencies List of optional dependency ids
  101. * @memberof module:@ui5/project.ui5Framework
  102. */
  103. /**
  104. * Install result
  105. *
  106. * @example
  107. * const resolverInstallResult = {
  108. * "libraryMetadata": {
  109. * "sap.ui.core": {
  110. * // ...
  111. * },
  112. * "sap.m": {
  113. * // ...
  114. * }
  115. * }
  116. * };
  117. *
  118. * @public
  119. * @typedef {object} ResolverInstallResult
  120. * @property {object.<string, module:@ui5/project.ui5Framework.LibraryMetadataEntry>} libraryMetadata
  121. * Object containing all installed libraries with library name as key
  122. * @memberof module:@ui5/project.ui5Framework
  123. */
  124. /**
  125. * Installs the provided libraries and their dependencies
  126. *
  127. * @example
  128. * const resolver = new Sapui5Resolver({version: "1.76.0"});
  129. * // Or for OpenUI5:
  130. * // const resolver = new Openui5Resolver({version: "1.76.0"});
  131. *
  132. * resolver.install(["sap.ui.core", "sap.m"]).then(({libraryMetadata}) => {
  133. * // Installation done
  134. * }).catch((err) => {
  135. * // Handle installation errors
  136. * });
  137. *
  138. * @public
  139. * @param {string[]} libraryNames List of library names to be installed
  140. * @returns {module:@ui5/project.ui5Framework.ResolverInstallResult}
  141. * Resolves with an object containing the <code>libraryMetadata</code>
  142. */
  143. async install(libraryNames) {
  144. const libraryMetadata = {};
  145. const errors = [];
  146. await this._processLibraries(libraryNames, libraryMetadata, errors);
  147. if (errors.length > 0) {
  148. throw new Error("Resolution of framework libraries failed with errors:\n" + errors.join("\n"));
  149. }
  150. return {
  151. libraryMetadata
  152. };
  153. }
  154. static async resolveVersion(version, {ui5HomeDir, cwd} = {}) {
  155. let spec;
  156. if (version === "latest") {
  157. spec = "*";
  158. } else if (VERSION_RANGE_REGEXP.test(version) || SEMVER_VERSION_REGEXP.test(version)) {
  159. spec = version;
  160. } else {
  161. throw new Error(`Framework version specifier "${version}" is incorrect or not supported`);
  162. }
  163. const versions = await this.fetchAllVersions({ui5HomeDir, cwd});
  164. const resolvedVersion = semver.maxSatisfying(versions, spec);
  165. if (!resolvedVersion) {
  166. if (semver.valid(spec)) {
  167. if (this.name === "Sapui5Resolver" && semver.lt(spec, "1.76.0")) {
  168. throw new Error(`Could not resolve framework version ${version}. ` +
  169. `Note that SAPUI5 framework libraries can only be consumed by the UI5 Tooling ` +
  170. `starting with SAPUI5 v1.76.0`);
  171. } else if (this.name === "Openui5Resolver" && semver.lt(spec, "1.52.5")) {
  172. throw new Error(`Could not resolve framework version ${version}. ` +
  173. `Note that OpenUI5 framework libraries can only be consumed by the UI5 Tooling ` +
  174. `starting with OpenUI5 v1.52.5`);
  175. }
  176. }
  177. throw new Error(`Could not resolve framework version ${version}`);
  178. }
  179. return resolvedVersion;
  180. }
  181. // To be implemented by resolver
  182. async getLibraryMetadata(libraryName) {
  183. throw new Error("AbstractResolver: getLibraryMetadata must be implemented!");
  184. }
  185. async handleLibrary(libraryName) {
  186. throw new Error("AbstractResolver: handleLibrary must be implemented!");
  187. }
  188. static fetchAllVersions(options) {
  189. throw new Error("AbstractResolver: static fetchAllVersions must be implemented!");
  190. }
  191. }
  192. if (process.env.NODE_ENV === "test") {
  193. // Export pattern for testing to be checked against JSON schema pattern
  194. AbstractResolver._SEMVER_VERSION_REGEXP = SEMVER_VERSION_REGEXP;
  195. }
  196. module.exports = AbstractResolver;