project/lib/graph/providers/NodePackageDependencies.js

  1. import path from "node:path";
  2. import {readPackageUp} from "read-pkg-up";
  3. import {readPackage} from "read-pkg";
  4. import {promisify} from "node:util";
  5. import fs from "graceful-fs";
  6. const realpath = promisify(fs.realpath);
  7. import resolve from "resolve";
  8. const resolveModulePath = promisify(resolve);
  9. import {getLogger} from "@ui5/logger";
  10. const log = getLogger("graph:providers:NodePackageDependencies");
  11. // Packages to consider:
  12. // * https://github.com/npm/read-package-json-fast
  13. // * https://github.com/npm/name-from-folder ?
  14. /**
  15. * @public
  16. * @class
  17. * @alias @ui5/project/graph/providers/NodePackageDependencies
  18. */
  19. class NodePackageDependencies {
  20. /**
  21. * Generates a project graph from npm modules
  22. *
  23. * @public
  24. * @param {object} options
  25. * @param {string} options.cwd Directory to start searching for the root module
  26. * @param {object} [options.rootConfiguration]
  27. * Configuration object to use for the root module instead of reading from a configuration file
  28. * @param {string} [options.rootConfigPath]
  29. * Configuration file to use for the root module instead the default ui5.yaml
  30. */
  31. constructor({cwd, rootConfiguration, rootConfigPath}) {
  32. this._cwd = cwd;
  33. this._rootConfiguration = rootConfiguration;
  34. this._rootConfigPath = rootConfigPath;
  35. }
  36. async getRootNode() {
  37. const rootPkg = await readPackageUp({
  38. cwd: this._cwd,
  39. normalize: false
  40. });
  41. if (!rootPkg || !rootPkg.packageJson) {
  42. throw new Error(
  43. `Failed to locate package.json for directory ${path.resolve(this._cwd)}`);
  44. }
  45. const modulePath = path.dirname(rootPkg.path);
  46. if (!rootPkg.packageJson.name) {
  47. throw new Error(`Missing or empty 'name' attribute in package.json at ${modulePath}`);
  48. }
  49. if (!rootPkg.packageJson.version) {
  50. throw new Error(`Missing or empty 'version' attribute in package.json at ${modulePath}`);
  51. }
  52. return {
  53. id: rootPkg.packageJson.name,
  54. version: rootPkg.packageJson.version,
  55. path: modulePath,
  56. configuration: this._rootConfiguration,
  57. configPath: this._rootConfigPath,
  58. _dependencies: await this._getDependencies(modulePath, rootPkg.packageJson, true)
  59. };
  60. }
  61. async getDependencies(node, workspace) {
  62. log.verbose(`Resolving dependencies of ${node.id}...`);
  63. if (!node._dependencies) {
  64. return [];
  65. }
  66. return Promise.all(node._dependencies.map(async ({name, optional}) => {
  67. const modulePath = await this._resolveModulePath(node.path, name, workspace);
  68. return this._getNode(modulePath, optional, name);
  69. }));
  70. }
  71. async _resolveModulePath(baseDir, moduleName, workspace) {
  72. log.verbose(`Resolving module path for '${moduleName}'...`);
  73. if (workspace) {
  74. // Check whether node can be resolved via the provided Workspace instance
  75. // If yes, replace the node from NodeProvider with the one from Workspace
  76. const workspaceNode = await workspace.getModuleByNodeId(moduleName);
  77. if (workspaceNode) {
  78. log.info(`Resolved module ${moduleName} via ${workspace.getName()} workspace ` +
  79. `to version ${workspaceNode.getVersion()}`);
  80. log.verbose(` Resolved module ${moduleName} to path ${workspaceNode.getPath()}`);
  81. return workspaceNode.getPath();
  82. }
  83. }
  84. try {
  85. let packageJsonPath = await resolveModulePath(moduleName + "/package.json", {
  86. basedir: baseDir,
  87. preserveSymlinks: false
  88. });
  89. packageJsonPath = await realpath(packageJsonPath);
  90. const modulePath = path.dirname(packageJsonPath);
  91. log.verbose(`Resolved module ${moduleName} to path ${modulePath}`);
  92. return modulePath;
  93. } catch (err) {
  94. throw new Error(
  95. `Unable to locate module ${moduleName} via resolve logic: ${err.message}`);
  96. }
  97. }
  98. /**
  99. * Resolves a Node module by reading its package.json
  100. *
  101. * @param {string} modulePath Path to the module.
  102. * @param {boolean} optional Whether this dependency is optional.
  103. * @param {string} [nameAlias] The name of the module. It's usually the same as the name definfed
  104. * in package.json and could easily be skipped. Useful when defining dependency as an alias:
  105. * {@link https://github.com/npm/rfcs/blob/main/implemented/0001-package-aliases.md}
  106. * @returns {Promise<object>}
  107. */
  108. async _getNode(modulePath, optional, nameAlias) {
  109. log.verbose(`Reading package.json in directory ${modulePath}...`);
  110. const packageJson = await readPackage({
  111. cwd: modulePath,
  112. normalize: false
  113. });
  114. return {
  115. id: nameAlias || packageJson.name,
  116. version: packageJson.version,
  117. path: modulePath,
  118. optional,
  119. _dependencies: await this._getDependencies(modulePath, packageJson)
  120. };
  121. }
  122. async _getDependencies(modulePath, packageJson, rootModule = false) {
  123. const dependencies = [];
  124. if (packageJson.dependencies) {
  125. Object.keys(packageJson.dependencies).forEach((depName) => {
  126. dependencies.push({
  127. name: depName,
  128. optional: false
  129. });
  130. });
  131. }
  132. if (rootModule && packageJson.devDependencies) {
  133. Object.keys(packageJson.devDependencies).forEach((depName) => {
  134. dependencies.push({
  135. name: depName,
  136. optional: false
  137. });
  138. });
  139. }
  140. if (!rootModule && packageJson.devDependencies) {
  141. await Promise.all(Object.keys(packageJson.devDependencies).map(async (depName) => {
  142. try {
  143. await this._resolveModulePath(modulePath, depName);
  144. dependencies.push({
  145. name: depName,
  146. optional: true
  147. });
  148. } catch (err) {
  149. // Ignore error since it's a development dependency of a non-root module
  150. }
  151. }));
  152. }
  153. if (packageJson.optionalDependencies) {
  154. await Promise.all(Object.keys(packageJson.optionalDependencies).map(async (depName) => {
  155. try {
  156. await this._resolveModulePath(modulePath, depName);
  157. dependencies.push({
  158. name: depName,
  159. optional: false
  160. });
  161. } catch (err) {
  162. // Ignore error since it's an optional dependency
  163. }
  164. }));
  165. }
  166. return dependencies;
  167. }
  168. }
  169. export default NodePackageDependencies;