finalize-manifest.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. 'use strict'
  2. const BB = require('bluebird')
  3. const cacache = require('cacache')
  4. const cacheKey = require('./util/cache-key')
  5. const fetchFromManifest = require('./fetch').fromManifest
  6. const finished = require('./util/finished')
  7. const minimatch = require('minimatch')
  8. const normalize = require('normalize-package-data')
  9. const optCheck = require('./util/opt-check')
  10. const path = require('path')
  11. const pipe = BB.promisify(require('mississippi').pipe)
  12. const ssri = require('ssri')
  13. const tar = require('tar')
  14. const readJson = require('./util/read-json')
  15. // `finalizeManifest` takes as input the various kinds of manifests that
  16. // manifest handlers ('lib/fetchers/*.js#manifest()') return, and makes sure
  17. // they are:
  18. //
  19. // * filled out with any required data that the handler couldn't fill in
  20. // * formatted consistently
  21. // * cached so we don't have to repeat this work more than necessary
  22. //
  23. // The biggest thing this package might do is do a full tarball extraction in
  24. // order to find missing bits of metadata required by the npm installer. For
  25. // example, it will fill in `_shrinkwrap`, `_integrity`, and other details that
  26. // the plain manifest handlers would require a tarball to fill out. If a
  27. // handler returns everything necessary, this process is skipped.
  28. //
  29. // If we get to the tarball phase, the corresponding tarball handler for the
  30. // requested type will be invoked and the entire tarball will be read from the
  31. // stream.
  32. //
  33. module.exports = finalizeManifest
  34. function finalizeManifest (pkg, spec, opts) {
  35. const key = finalKey(pkg, spec)
  36. opts = optCheck(opts)
  37. const cachedManifest = (opts.cache && key && !opts.preferOnline && !opts.fullMetadata && !opts.enjoyBy)
  38. ? cacache.get.info(opts.cache, key, opts)
  39. : BB.resolve(null)
  40. return cachedManifest.then(cached => {
  41. if (cached && cached.metadata && cached.metadata.manifest) {
  42. return new Manifest(cached.metadata.manifest)
  43. } else {
  44. return tarballedProps(pkg, spec, opts).then(props => {
  45. return pkg && pkg.name
  46. ? new Manifest(pkg, props, opts.fullMetadata)
  47. : new Manifest(props, null, opts.fullMetadata)
  48. }).then(manifest => {
  49. const cacheKey = key || finalKey(manifest, spec)
  50. if (!opts.cache || !cacheKey) {
  51. return manifest
  52. } else {
  53. return cacache.put(
  54. opts.cache, cacheKey, '.', {
  55. metadata: {
  56. id: manifest._id,
  57. manifest,
  58. type: 'finalized-manifest'
  59. }
  60. }
  61. ).then(() => manifest)
  62. }
  63. })
  64. }
  65. })
  66. }
  67. module.exports.Manifest = Manifest
  68. function Manifest (pkg, fromTarball, fullMetadata) {
  69. fromTarball = fromTarball || {}
  70. if (fullMetadata) {
  71. Object.assign(this, pkg)
  72. }
  73. this.name = pkg.name
  74. this.version = pkg.version
  75. this.engines = pkg.engines || fromTarball.engines
  76. this.cpu = pkg.cpu || fromTarball.cpu
  77. this.os = pkg.os || fromTarball.os
  78. this.dependencies = pkg.dependencies || {}
  79. this.optionalDependencies = pkg.optionalDependencies || {}
  80. this.devDependencies = pkg.devDependencies || {}
  81. const bundled = (
  82. pkg.bundledDependencies ||
  83. pkg.bundleDependencies ||
  84. false
  85. )
  86. this.bundleDependencies = bundled
  87. this.peerDependencies = pkg.peerDependencies || {}
  88. this.deprecated = pkg.deprecated || false
  89. // These depend entirely on each handler
  90. this._resolved = pkg._resolved
  91. // Not all handlers (or registries) provide these out of the box,
  92. // and if they don't, we need to extract and read the tarball ourselves.
  93. // These are details required by the installer.
  94. this._integrity = pkg._integrity || fromTarball._integrity || null
  95. this._shasum = pkg._shasum || fromTarball._shasum || null
  96. this._shrinkwrap = pkg._shrinkwrap || fromTarball._shrinkwrap || null
  97. this.bin = pkg.bin || fromTarball.bin || null
  98. if (this.bin && Array.isArray(this.bin)) {
  99. // Code yanked from read-package-json.
  100. const m = (pkg.directories && pkg.directories.bin) || '.'
  101. this.bin = this.bin.reduce((acc, mf) => {
  102. if (mf && mf.charAt(0) !== '.') {
  103. const f = path.basename(mf)
  104. acc[f] = path.join(m, mf)
  105. }
  106. return acc
  107. }, {})
  108. }
  109. this._id = null
  110. // TODO - freezing and inextensibility pending npm changes. See test suite.
  111. // Object.preventExtensions(this)
  112. normalize(this)
  113. // I don't want this why did you give it to me. Go away. 🔥🔥🔥🔥
  114. delete this.readme
  115. // Object.freeze(this)
  116. }
  117. // Some things aren't filled in by standard manifest fetching.
  118. // If this function needs to do its work, it will grab the
  119. // package tarball, extract it, and take whatever it needs
  120. // from the stream.
  121. function tarballedProps (pkg, spec, opts) {
  122. const needsShrinkwrap = (!pkg || (
  123. pkg._hasShrinkwrap !== false &&
  124. !pkg._shrinkwrap
  125. ))
  126. const needsBin = !!(!pkg || (
  127. !pkg.bin &&
  128. pkg.directories &&
  129. pkg.directories.bin
  130. ))
  131. const needsIntegrity = !pkg || (!pkg._integrity && pkg._integrity !== false)
  132. const needsShasum = !pkg || (!pkg._shasum && pkg._shasum !== false)
  133. const needsHash = needsIntegrity || needsShasum
  134. const needsManifest = !pkg || !pkg.name
  135. const needsExtract = needsShrinkwrap || needsBin || needsManifest
  136. if (!needsShrinkwrap && !needsBin && !needsHash && !needsManifest) {
  137. return BB.resolve({})
  138. } else {
  139. opts = optCheck(opts)
  140. const tarStream = fetchFromManifest(pkg, spec, opts)
  141. const extracted = needsExtract && new tar.Parse()
  142. return BB.join(
  143. needsShrinkwrap && jsonFromStream('npm-shrinkwrap.json', extracted),
  144. needsManifest && jsonFromStream('package.json', extracted),
  145. needsBin && getPaths(extracted),
  146. needsHash && ssri.fromStream(tarStream, { algorithms: ['sha1', 'sha512'] }),
  147. needsExtract && pipe(tarStream, extracted),
  148. (sr, mani, paths, hash) => {
  149. if (needsManifest && !mani) {
  150. const err = new Error(`Non-registry package missing package.json: ${spec}.`)
  151. err.code = 'ENOPACKAGEJSON'
  152. throw err
  153. }
  154. const extraProps = mani || {}
  155. delete extraProps._resolved
  156. // drain out the rest of the tarball
  157. tarStream.resume()
  158. // if we have directories.bin, we need to collect any matching files
  159. // to add to bin
  160. if (paths && paths.length) {
  161. const dirBin = mani
  162. ? (mani && mani.directories && mani.directories.bin)
  163. : (pkg && pkg.directories && pkg.directories.bin)
  164. if (dirBin) {
  165. extraProps.bin = {}
  166. paths.forEach(filePath => {
  167. if (minimatch(filePath, dirBin + '/**')) {
  168. const relative = path.relative(dirBin, filePath)
  169. if (relative && relative[0] !== '.') {
  170. extraProps.bin[path.basename(relative)] = path.join(dirBin, relative)
  171. }
  172. }
  173. })
  174. }
  175. }
  176. return Object.assign(extraProps, {
  177. _shrinkwrap: sr,
  178. _resolved: (mani && mani._resolved) ||
  179. (pkg && pkg._resolved) ||
  180. spec.fetchSpec,
  181. _integrity: needsIntegrity && hash && hash.sha512 && hash.sha512[0].toString(),
  182. _shasum: needsShasum && hash && hash.sha1 && hash.sha1[0].hexDigest()
  183. })
  184. }
  185. )
  186. }
  187. }
  188. function jsonFromStream (filename, dataStream) {
  189. return BB.fromNode(cb => {
  190. dataStream.on('error', cb)
  191. dataStream.on('close', cb)
  192. dataStream.on('entry', entry => {
  193. const filePath = entry.header.path.replace(/[^/]+\//, '')
  194. if (filePath !== filename) {
  195. entry.resume()
  196. } else {
  197. let data = ''
  198. entry.on('error', cb)
  199. finished(entry).then(() => {
  200. try {
  201. cb(null, readJson(data))
  202. } catch (err) {
  203. cb(err)
  204. }
  205. }, err => {
  206. cb(err)
  207. })
  208. entry.on('data', d => { data += d })
  209. }
  210. })
  211. })
  212. }
  213. function getPaths (dataStream) {
  214. return BB.fromNode(cb => {
  215. let paths = []
  216. dataStream.on('error', cb)
  217. dataStream.on('close', () => cb(null, paths))
  218. dataStream.on('entry', function handler (entry) {
  219. const filePath = entry.header.path.replace(/[^/]+\//, '')
  220. entry.resume()
  221. paths.push(filePath)
  222. })
  223. })
  224. }
  225. function finalKey (pkg, spec) {
  226. if (pkg && pkg._uniqueResolved) {
  227. // git packages have a unique, identifiable id, but no tar sha
  228. return cacheKey(`${spec.type}-manifest`, pkg._uniqueResolved)
  229. } else {
  230. return (
  231. pkg && pkg._integrity &&
  232. cacheKey(
  233. `${spec.type}-manifest`,
  234. `${pkg._resolved}:${ssri.stringify(pkg._integrity)}`
  235. )
  236. )
  237. }
  238. }