index.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. "use strict";
  2. const { cosmiconfigSync } = require("cosmiconfig");
  3. const dedent = require("dedent");
  4. const globby = require("globby");
  5. const globParent = require("glob-parent");
  6. const loadJsonFile = require("load-json-file");
  7. const log = require("npmlog");
  8. const pMap = require("p-map");
  9. const path = require("path");
  10. const writeJsonFile = require("write-json-file");
  11. const { ValidationError } = require("@lerna/validation-error");
  12. const { Package } = require("@lerna/package");
  13. const { applyExtends } = require("./lib/apply-extends");
  14. const { deprecateConfig } = require("./lib/deprecate-config");
  15. const { makeFileFinder, makeSyncFileFinder } = require("./lib/make-file-finder");
  16. /**
  17. * @typedef {object} ProjectConfig
  18. * @property {string[]} packages
  19. * @property {boolean} useWorkspaces
  20. * @property {string} version
  21. */
  22. /**
  23. * A representation of the entire project managed by Lerna.
  24. *
  25. * Wherever the lerna.json file is located, that is the project root.
  26. * All package globs are rooted from this location.
  27. */
  28. class Project {
  29. /**
  30. * @param {string} [cwd] Defaults to process.cwd()
  31. */
  32. static getPackages(cwd) {
  33. return new Project(cwd).getPackages();
  34. }
  35. /**
  36. * @param {string} [cwd] Defaults to process.cwd()
  37. */
  38. static getPackagesSync(cwd) {
  39. return new Project(cwd).getPackagesSync();
  40. }
  41. /**
  42. * @param {string} [cwd] Defaults to process.cwd()
  43. */
  44. constructor(cwd) {
  45. const explorer = cosmiconfigSync("lerna", {
  46. searchPlaces: ["lerna.json", "package.json"],
  47. transform(obj) {
  48. // cosmiconfig returns null when nothing is found
  49. if (!obj) {
  50. return {
  51. // No need to distinguish between missing and empty,
  52. // saves a lot of noisy guards elsewhere
  53. config: {},
  54. // path.resolve(".", ...) starts from process.cwd()
  55. filepath: path.resolve(cwd || ".", "lerna.json"),
  56. };
  57. }
  58. // rename deprecated durable config
  59. deprecateConfig(obj.config, obj.filepath);
  60. obj.config = applyExtends(obj.config, path.dirname(obj.filepath));
  61. return obj;
  62. },
  63. });
  64. let loaded;
  65. try {
  66. loaded = explorer.search(cwd);
  67. } catch (err) {
  68. // redecorate JSON syntax errors, avoid debug dump
  69. if (err.name === "JSONError") {
  70. throw new ValidationError(err.name, err.message);
  71. }
  72. // re-throw other errors, could be ours or third-party
  73. throw err;
  74. }
  75. /** @type {ProjectConfig} */
  76. this.config = loaded.config;
  77. this.rootConfigLocation = loaded.filepath;
  78. this.rootPath = path.dirname(loaded.filepath);
  79. log.verbose("rootPath", this.rootPath);
  80. }
  81. get version() {
  82. return this.config.version;
  83. }
  84. set version(val) {
  85. this.config.version = val;
  86. }
  87. get packageConfigs() {
  88. if (this.config.useWorkspaces) {
  89. const workspaces = this.manifest.get("workspaces");
  90. if (!workspaces) {
  91. throw new ValidationError(
  92. "EWORKSPACES",
  93. dedent`
  94. Yarn workspaces need to be defined in the root package.json.
  95. See: https://github.com/lerna/lerna/blob/master/commands/bootstrap/README.md#--use-workspaces
  96. `
  97. );
  98. }
  99. return workspaces.packages || workspaces;
  100. }
  101. return this.config.packages || [Project.PACKAGE_GLOB];
  102. }
  103. get packageParentDirs() {
  104. return this.packageConfigs.map(globParent).map((parentDir) => path.resolve(this.rootPath, parentDir));
  105. }
  106. get manifest() {
  107. let manifest;
  108. try {
  109. const manifestLocation = path.join(this.rootPath, "package.json");
  110. const packageJson = loadJsonFile.sync(manifestLocation);
  111. if (!packageJson.name) {
  112. // npm-lifecycle chokes if this is missing, so default like npm init does
  113. packageJson.name = path.basename(path.dirname(manifestLocation));
  114. }
  115. // Encapsulate raw JSON in Package instance
  116. manifest = new Package(packageJson, this.rootPath);
  117. // redefine getter to lazy-loaded value
  118. Object.defineProperty(this, "manifest", {
  119. value: manifest,
  120. });
  121. } catch (err) {
  122. // redecorate JSON syntax errors, avoid debug dump
  123. if (err.name === "JSONError") {
  124. throw new ValidationError(err.name, err.message);
  125. }
  126. // try again next time
  127. }
  128. return manifest;
  129. }
  130. get licensePath() {
  131. let licensePath;
  132. try {
  133. const search = globby.sync(Project.LICENSE_GLOB, {
  134. cwd: this.rootPath,
  135. absolute: true,
  136. caseSensitiveMatch: false,
  137. // Project license is always a sibling of the root manifest
  138. deep: 0,
  139. });
  140. licensePath = search.shift();
  141. if (licensePath) {
  142. // POSIX results always need to be normalized
  143. licensePath = path.normalize(licensePath);
  144. // redefine getter to lazy-loaded value
  145. Object.defineProperty(this, "licensePath", {
  146. value: licensePath,
  147. });
  148. }
  149. } catch (err) {
  150. /* istanbul ignore next */
  151. throw new ValidationError(err.name, err.message);
  152. }
  153. return licensePath;
  154. }
  155. get fileFinder() {
  156. const finder = makeFileFinder(this.rootPath, this.packageConfigs);
  157. // redefine getter to lazy-loaded value
  158. Object.defineProperty(this, "fileFinder", {
  159. value: finder,
  160. });
  161. return finder;
  162. }
  163. /**
  164. * @returns {Promise<Package[]>} A promise resolving to a list of Package instances
  165. */
  166. getPackages() {
  167. const mapper = (packageConfigPath) =>
  168. loadJsonFile(packageConfigPath).then(
  169. (packageJson) => new Package(packageJson, path.dirname(packageConfigPath), this.rootPath)
  170. );
  171. return this.fileFinder("package.json", (filePaths) => pMap(filePaths, mapper, { concurrency: 50 }));
  172. }
  173. /**
  174. * @returns {Package[]} A list of Package instances
  175. */
  176. getPackagesSync() {
  177. return makeSyncFileFinder(this.rootPath, this.packageConfigs)("package.json", (packageConfigPath) => {
  178. return new Package(
  179. loadJsonFile.sync(packageConfigPath),
  180. path.dirname(packageConfigPath),
  181. this.rootPath
  182. );
  183. });
  184. }
  185. getPackageLicensePaths() {
  186. return this.fileFinder(Project.LICENSE_GLOB, null, { caseSensitiveMatch: false });
  187. }
  188. isIndependent() {
  189. return this.version === "independent";
  190. }
  191. serializeConfig() {
  192. // TODO: might be package.json prop
  193. return writeJsonFile(this.rootConfigLocation, this.config, { indent: 2, detectIndent: true }).then(
  194. () => this.rootConfigLocation
  195. );
  196. }
  197. }
  198. Project.PACKAGE_GLOB = "packages/*";
  199. Project.LICENSE_GLOB = "LICEN{S,C}E{,.*}";
  200. module.exports.Project = Project;
  201. module.exports.getPackages = Project.getPackages;
  202. module.exports.getPackagesSync = Project.getPackagesSync;