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