index.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. "use strict";
  2. const npa = require("npm-package-arg");
  3. const path = require("path");
  4. const loadJsonFile = require("load-json-file");
  5. const writePkg = require("write-pkg");
  6. // symbol used to "hide" internal state
  7. const PKG = Symbol("pkg");
  8. /* eslint-disable no-underscore-dangle */
  9. // private fields
  10. const _location = Symbol("location");
  11. const _resolved = Symbol("resolved");
  12. const _rootPath = Symbol("rootPath");
  13. const _scripts = Symbol("scripts");
  14. const _contents = Symbol("contents");
  15. /**
  16. * @param {import("npm-package-arg").Result} result
  17. */
  18. function binSafeName({ name, scope }) {
  19. return scope ? name.substring(scope.length + 1) : name;
  20. }
  21. // package.json files are not that complicated, so this is intentionally naïve
  22. function shallowCopy(json) {
  23. return Object.keys(json).reduce((obj, key) => {
  24. const val = json[key];
  25. /* istanbul ignore if */
  26. if (Array.isArray(val)) {
  27. obj[key] = val.slice();
  28. } else if (val && typeof val === "object") {
  29. obj[key] = Object.assign({}, val);
  30. } else {
  31. obj[key] = val;
  32. }
  33. return obj;
  34. }, {});
  35. }
  36. /**
  37. * @typedef {object} RawManifest The subset of package.json properties that Lerna uses
  38. * @property {string} name
  39. * @property {string} version
  40. * @property {boolean} [private]
  41. * @property {Record<string, string>|string} [bin]
  42. * @property {Record<string, string>} [scripts]
  43. * @property {Record<string, string>} [dependencies]
  44. * @property {Record<string, string>} [devDependencies]
  45. * @property {Record<string, string>} [optionalDependencies]
  46. * @property {Record<string, string>} [peerDependencies]
  47. * @property {Record<'directory' | 'registry' | 'tag', string>} [publishConfig]
  48. * @property {string[] | { packages: string[] }} [workspaces]
  49. */
  50. /**
  51. * Lerna's internal representation of a local package, with
  52. * many values resolved directly from the original JSON.
  53. */
  54. class Package {
  55. /**
  56. * Create a Package instance from parameters, possibly reusing existing instance.
  57. * @param {string|Package|RawManifest} ref A path to a package.json file, Package instance, or JSON object
  58. * @param {string} [dir] If `ref` is a JSON object, this is the location of the manifest
  59. * @returns {Package}
  60. */
  61. static lazy(ref, dir = ".") {
  62. if (typeof ref === "string") {
  63. const location = path.resolve(path.basename(ref) === "package.json" ? path.dirname(ref) : ref);
  64. const manifest = loadJsonFile.sync(path.join(location, "package.json"));
  65. return new Package(manifest, location);
  66. }
  67. // don't use instanceof because it fails across nested module boundaries
  68. if ("__isLernaPackage" in ref) {
  69. return ref;
  70. }
  71. // assume ref is a json object
  72. return new Package(ref, dir);
  73. }
  74. /**
  75. * @param {RawManifest} pkg
  76. * @param {string} location
  77. * @param {string} [rootPath]
  78. */
  79. constructor(pkg, location, rootPath = location) {
  80. // npa will throw an error if the name is invalid
  81. const resolved = npa.resolve(pkg.name, `file:${path.relative(rootPath, location)}`, rootPath);
  82. this.name = pkg.name;
  83. this[PKG] = pkg;
  84. // omit raw pkg from default util.inspect() output, but preserve internal mutability
  85. Object.defineProperty(this, PKG, { enumerable: false, writable: true });
  86. this[_location] = location;
  87. this[_resolved] = resolved;
  88. this[_rootPath] = rootPath;
  89. this[_scripts] = { ...pkg.scripts };
  90. }
  91. // readonly getters
  92. get location() {
  93. return this[_location];
  94. }
  95. get private() {
  96. return Boolean(this[PKG].private);
  97. }
  98. get resolved() {
  99. return this[_resolved];
  100. }
  101. get rootPath() {
  102. return this[_rootPath];
  103. }
  104. get scripts() {
  105. return this[_scripts];
  106. }
  107. get bin() {
  108. const pkg = this[PKG];
  109. return typeof pkg.bin === "string"
  110. ? {
  111. [binSafeName(this.resolved)]: pkg.bin,
  112. }
  113. : Object.assign({}, pkg.bin);
  114. }
  115. get binLocation() {
  116. return path.join(this.location, "node_modules", ".bin");
  117. }
  118. get manifestLocation() {
  119. return path.join(this.location, "package.json");
  120. }
  121. get nodeModulesLocation() {
  122. return path.join(this.location, "node_modules");
  123. }
  124. // eslint-disable-next-line class-methods-use-this
  125. get __isLernaPackage() {
  126. // safer than instanceof across module boundaries
  127. return true;
  128. }
  129. // accessors
  130. get version() {
  131. return this[PKG].version;
  132. }
  133. set version(version) {
  134. this[PKG].version = version;
  135. }
  136. get contents() {
  137. // if modified with setter, use that value
  138. if (this[_contents]) {
  139. return this[_contents];
  140. }
  141. // if provided by pkg.publishConfig.directory value
  142. if (this[PKG].publishConfig && this[PKG].publishConfig.directory) {
  143. return path.join(this.location, this[PKG].publishConfig.directory);
  144. }
  145. // default to package root
  146. return this.location;
  147. }
  148. set contents(subDirectory) {
  149. this[_contents] = path.join(this.location, subDirectory);
  150. }
  151. // "live" collections
  152. get dependencies() {
  153. return this[PKG].dependencies;
  154. }
  155. get devDependencies() {
  156. return this[PKG].devDependencies;
  157. }
  158. get optionalDependencies() {
  159. return this[PKG].optionalDependencies;
  160. }
  161. get peerDependencies() {
  162. return this[PKG].peerDependencies;
  163. }
  164. /**
  165. * Map-like retrieval of arbitrary values
  166. * @template {keyof RawManifest} K
  167. * @param {K} key field name to retrieve value
  168. * @returns {RawManifest[K]} value stored under key, if present
  169. */
  170. get(key) {
  171. return this[PKG][key];
  172. }
  173. /**
  174. * Map-like storage of arbitrary values
  175. * @template {keyof RawManifest} K
  176. * @param {T} key field name to store value
  177. * @param {RawManifest[K]} val value to store
  178. * @returns {Package} instance for chaining
  179. */
  180. set(key, val) {
  181. this[PKG][key] = val;
  182. return this;
  183. }
  184. /**
  185. * Provide shallow copy for munging elsewhere
  186. * @returns {Object}
  187. */
  188. toJSON() {
  189. return shallowCopy(this[PKG]);
  190. }
  191. /**
  192. * Refresh internal state from disk (e.g., changed by external lifecycles)
  193. */
  194. refresh() {
  195. return loadJsonFile(this.manifestLocation).then((pkg) => {
  196. this[PKG] = pkg;
  197. return this;
  198. });
  199. }
  200. /**
  201. * Write manifest changes to disk
  202. * @returns {Promise} resolves when write finished
  203. */
  204. serialize() {
  205. return writePkg(this.manifestLocation, this[PKG]).then(() => this);
  206. }
  207. /**
  208. * Mutate local dependency spec according to type
  209. * @param {Object} resolved npa metadata
  210. * @param {String} depVersion semver
  211. * @param {String} savePrefix npm_config_save_prefix
  212. */
  213. updateLocalDependency(resolved, depVersion, savePrefix) {
  214. const depName = resolved.name;
  215. // first, try runtime dependencies
  216. let depCollection = this.dependencies;
  217. // try optionalDependencies if that didn't work
  218. if (!depCollection || !depCollection[depName]) {
  219. depCollection = this.optionalDependencies;
  220. }
  221. // fall back to devDependencies
  222. if (!depCollection || !depCollection[depName]) {
  223. depCollection = this.devDependencies;
  224. }
  225. if (resolved.registry || resolved.type === "directory") {
  226. // a version (1.2.3) OR range (^1.2.3) OR directory (file:../foo-pkg)
  227. depCollection[depName] = `${savePrefix}${depVersion}`;
  228. } else if (resolved.gitCommittish) {
  229. // a git url with matching committish (#v1.2.3 or #1.2.3)
  230. const [tagPrefix] = /^\D*/.exec(resolved.gitCommittish);
  231. // update committish
  232. const { hosted } = resolved; // take that, lint!
  233. hosted.committish = `${tagPrefix}${depVersion}`;
  234. // always serialize the full url (identical to previous resolved.saveSpec)
  235. depCollection[depName] = hosted.toString({ noGitPlus: false, noCommittish: false });
  236. } else if (resolved.gitRange) {
  237. // a git url with matching gitRange (#semver:^1.2.3)
  238. const { hosted } = resolved; // take that, lint!
  239. hosted.committish = `semver:${savePrefix}${depVersion}`;
  240. // always serialize the full url (identical to previous resolved.saveSpec)
  241. depCollection[depName] = hosted.toString({ noGitPlus: false, noCommittish: false });
  242. }
  243. }
  244. }
  245. module.exports.Package = Package;