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