1'use strict' 2 3const BB = require('bluebird') 4 5let addBundled 6const childPath = require('../utils/child-path.js') 7const createChild = require('./node.js').create 8let fetchPackageMetadata 9const inflateBundled = require('./inflate-bundled.js') 10const moduleName = require('../utils/module-name.js') 11const normalizePackageData = require('normalize-package-data') 12const npm = require('../npm.js') 13const realizeShrinkwrapSpecifier = require('./realize-shrinkwrap-specifier.js') 14const validate = require('aproba') 15const path = require('path') 16const isRegistry = require('../utils/is-registry.js') 17const hasModernMeta = require('./has-modern-meta.js') 18const ssri = require('ssri') 19const npa = require('npm-package-arg') 20 21module.exports = function (tree, sw, opts, finishInflating) { 22 if (!fetchPackageMetadata) { 23 fetchPackageMetadata = BB.promisify(require('../fetch-package-metadata.js')) 24 addBundled = BB.promisify(fetchPackageMetadata.addBundled) 25 } 26 if (arguments.length === 3) { 27 finishInflating = opts 28 opts = {} 29 } 30 if (!npm.config.get('shrinkwrap') || !npm.config.get('package-lock')) { 31 return finishInflating() 32 } 33 tree.loaded = false 34 tree.hasRequiresFromLock = sw.requires 35 return inflateShrinkwrap(tree.path, tree, sw.dependencies, opts).then( 36 () => finishInflating(), 37 finishInflating 38 ) 39} 40 41function inflateShrinkwrap (topPath, tree, swdeps, opts) { 42 if (!swdeps) return Promise.resolve() 43 if (!opts) opts = {} 44 const onDisk = {} 45 tree.children.forEach((child) => { 46 onDisk[moduleName(child)] = child 47 }) 48 49 tree.children = [] 50 51 return BB.each(Object.keys(swdeps), (name) => { 52 const sw = swdeps[name] 53 const dependencies = sw.dependencies || {} 54 const requested = realizeShrinkwrapSpecifier(name, sw, topPath) 55 56 if (Object.keys(sw).length === 0) { 57 let message = `Object for dependency "${name}" is empty.\n` 58 message += 'Something went wrong. Regenerate the package-lock.json with "npm install".\n' 59 message += 'If using a shrinkwrap, regenerate with "npm shrinkwrap".' 60 return Promise.reject(new Error(message)) 61 } 62 63 return inflatableChild( 64 onDisk[name], name, topPath, tree, sw, requested, opts 65 ).then((child) => { 66 child.hasRequiresFromLock = tree.hasRequiresFromLock 67 return inflateShrinkwrap(topPath, child, dependencies) 68 }) 69 }) 70} 71 72function normalizePackageDataNoErrors (pkg) { 73 try { 74 normalizePackageData(pkg) 75 } catch (ex) { 76 // don't care 77 } 78} 79 80function quotemeta (str) { 81 return str.replace(/([^A-Za-z_0-9/])/g, '\\$1') 82} 83 84function tarballToVersion (name, tb) { 85 const registry = quotemeta(npm.config.get('registry') || '') 86 .replace(/https?:/, 'https?:') 87 .replace(/([^/])$/, '$1/') 88 let matchRegTarball 89 if (name) { 90 const nameMatch = quotemeta(name) 91 matchRegTarball = new RegExp(`^${registry}${nameMatch}/-/${nameMatch}-(.*)[.]tgz$`) 92 } else { 93 matchRegTarball = new RegExp(`^${registry}(.*)?/-/\\1-(.*)[.]tgz$`) 94 } 95 const match = tb.match(matchRegTarball) 96 if (!match) return 97 return match[2] || match[1] 98} 99 100function relativizeLink (name, spec, topPath, requested) { 101 if (!spec.startsWith('file:')) { 102 return 103 } 104 105 let requestedPath = requested.fetchSpec 106 if (requested.type === 'file') { 107 requestedPath = path.dirname(requestedPath) 108 } 109 110 const relativized = path.relative(requestedPath, path.resolve(topPath, spec.slice(5))) 111 return 'file:' + relativized 112} 113 114function inflatableChild (onDiskChild, name, topPath, tree, sw, requested, opts) { 115 validate('OSSOOOO|ZSSOOOO', arguments) 116 const usesIntegrity = ( 117 requested.registry || 118 requested.type === 'remote' || 119 requested.type === 'file' 120 ) 121 const regTarball = tarballToVersion(name, sw.version) 122 if (regTarball) { 123 sw.resolved = sw.version 124 sw.version = regTarball 125 } 126 if (sw.requires) { 127 Object.keys(sw.requires).forEach(name => { 128 const spec = sw.requires[name] 129 sw.requires[name] = tarballToVersion(name, spec) || 130 relativizeLink(name, spec, topPath, requested) || 131 spec 132 }) 133 } 134 const modernLink = requested.type === 'directory' && !sw.from 135 if (hasModernMeta(onDiskChild) && childIsEquivalent(sw, requested, onDiskChild)) { 136 // The version on disk matches the shrinkwrap entry. 137 if (!onDiskChild.fromShrinkwrap) onDiskChild.fromShrinkwrap = requested 138 onDiskChild.package._requested = requested 139 onDiskChild.package._spec = requested.rawSpec 140 onDiskChild.package._where = topPath 141 onDiskChild.package._optional = sw.optional 142 onDiskChild.package._development = sw.dev 143 onDiskChild.package._inBundle = sw.bundled 144 onDiskChild.fromBundle = (sw.bundled || onDiskChild.package._inBundle) ? tree.fromBundle || tree : null 145 if (!onDiskChild.package._args) onDiskChild.package._args = [] 146 onDiskChild.package._args.push([String(requested), topPath]) 147 // non-npm registries can and will return unnormalized data, plus 148 // even the npm registry may have package data normalized with older 149 // normalization rules. This ensures we get package data in a consistent, 150 // stable format. 151 normalizePackageDataNoErrors(onDiskChild.package) 152 onDiskChild.swRequires = sw.requires 153 tree.children.push(onDiskChild) 154 return BB.resolve(onDiskChild) 155 } else if ((sw.version && (sw.integrity || !usesIntegrity) && (requested.type !== 'directory' || modernLink)) || sw.bundled) { 156 // The shrinkwrap entry has an integrity field. We can fake a pkg to get 157 // the installer to do a content-address fetch from the cache, if possible. 158 return BB.resolve(makeFakeChild(name, topPath, tree, sw, requested)) 159 } else { 160 // It's not on disk, and we can't just look it up by address -- do a full 161 // fpm/inflate bundle pass. For registry deps, this will go straight to the 162 // tarball URL, as if it were a remote tarball dep. 163 return fetchChild(topPath, tree, sw, requested) 164 } 165} 166 167function isGit (sw) { 168 const version = npa.resolve(sw.name, sw.version) 169 return (version && version.type === 'git') 170} 171 172function makeFakeChild (name, topPath, tree, sw, requested) { 173 const isDirectory = requested.type === 'directory' 174 const from = sw.from || requested.raw 175 const pkg = { 176 name: name, 177 version: sw.version, 178 _id: name + '@' + sw.version, 179 _resolved: sw.resolved || (isGit(sw) && sw.version), 180 _requested: requested, 181 _optional: sw.optional, 182 _development: sw.dev, 183 _inBundle: sw.bundled, 184 _integrity: sw.integrity, 185 _from: from, 186 _spec: requested.rawSpec, 187 _where: topPath, 188 _args: [[requested.toString(), topPath]], 189 dependencies: sw.requires 190 } 191 192 if (!sw.bundled) { 193 const bundleDependencies = Object.keys(sw.dependencies || {}).filter((d) => sw.dependencies[d].bundled) 194 if (bundleDependencies.length === 0) { 195 pkg.bundleDependencies = bundleDependencies 196 } 197 } 198 const child = createChild({ 199 package: pkg, 200 loaded: isDirectory, 201 parent: tree, 202 children: [], 203 fromShrinkwrap: requested, 204 fakeChild: sw, 205 fromBundle: sw.bundled ? tree.fromBundle || tree : null, 206 path: childPath(tree.path, pkg), 207 realpath: isDirectory ? requested.fetchSpec : childPath(tree.realpath, pkg), 208 location: (tree.location === '/' ? '' : tree.location + '/') + pkg.name, 209 isLink: isDirectory, 210 isInLink: tree.isLink || tree.isInLink, 211 swRequires: sw.requires 212 }) 213 tree.children.push(child) 214 return child 215} 216 217function fetchChild (topPath, tree, sw, requested) { 218 return fetchPackageMetadata(requested, topPath).then((pkg) => { 219 pkg._from = sw.from || requested.raw 220 pkg._optional = sw.optional 221 pkg._development = sw.dev 222 pkg._inBundle = false 223 return addBundled(pkg).then(() => pkg) 224 }).then((pkg) => { 225 var isLink = pkg._requested.type === 'directory' 226 const child = createChild({ 227 package: pkg, 228 loaded: false, 229 parent: tree, 230 fromShrinkwrap: requested, 231 path: childPath(tree.path, pkg), 232 realpath: isLink ? requested.fetchSpec : childPath(tree.realpath, pkg), 233 children: pkg._bundled || [], 234 location: (tree.location === '/' ? '' : tree.location + '/') + pkg.name, 235 fromBundle: null, 236 isLink: isLink, 237 isInLink: tree.isLink, 238 swRequires: sw.requires 239 }) 240 tree.children.push(child) 241 if (pkg._bundled) { 242 delete pkg._bundled 243 inflateBundled(child, child, child.children) 244 } 245 return child 246 }) 247} 248 249function childIsEquivalent (sw, requested, child) { 250 if (!child) return false 251 if (child.fromShrinkwrap) return true 252 if ( 253 sw.integrity && 254 child.package._integrity && 255 ssri.parse(sw.integrity).match(child.package._integrity) 256 ) return true 257 if (child.isLink && requested.type === 'directory') return path.relative(child.realpath, requested.fetchSpec) === '' 258 259 if (sw.resolved) return child.package._resolved === sw.resolved 260 if (!isRegistry(requested) && sw.from) return child.package._from === sw.from 261 if (!isRegistry(requested) && child.package._resolved) return sw.version === child.package._resolved 262 return child.package.version === sw.version 263} 264