1'use strict' 2 3const BB = require('bluebird') 4 5const chain = require('slide').chain 6const detectIndent = require('detect-indent') 7const detectNewline = require('detect-newline') 8const readFile = BB.promisify(require('graceful-fs').readFile) 9const getRequested = require('./install/get-requested.js') 10const id = require('./install/deps.js') 11const iferr = require('iferr') 12const isOnlyOptional = require('./install/is-only-optional.js') 13const isOnlyDev = require('./install/is-only-dev.js') 14const lifecycle = require('./utils/lifecycle.js') 15const log = require('npmlog') 16const moduleName = require('./utils/module-name.js') 17const move = require('move-concurrently') 18const npm = require('./npm.js') 19const path = require('path') 20const readPackageTree = BB.promisify(require('read-package-tree')) 21const ssri = require('ssri') 22const stringifyPackage = require('stringify-package') 23const validate = require('aproba') 24const writeFileAtomic = require('write-file-atomic') 25const unixFormatPath = require('./utils/unix-format-path.js') 26const isRegistry = require('./utils/is-registry.js') 27 28const { chown } = require('fs') 29const inferOwner = require('infer-owner') 30const selfOwner = { 31 uid: process.getuid && process.getuid(), 32 gid: process.getgid && process.getgid() 33} 34 35const PKGLOCK = 'package-lock.json' 36const SHRINKWRAP = 'npm-shrinkwrap.json' 37const PKGLOCK_VERSION = npm.lockfileVersion 38 39// emit JSON describing versions of all packages currently installed (for later 40// use with shrinkwrap install) 41shrinkwrap.usage = 'npm shrinkwrap' 42 43module.exports = exports = shrinkwrap 44exports.treeToShrinkwrap = treeToShrinkwrap 45 46function shrinkwrap (args, silent, cb) { 47 if (typeof cb !== 'function') { 48 cb = silent 49 silent = false 50 } 51 52 if (args.length) { 53 log.warn('shrinkwrap', "doesn't take positional args") 54 } 55 56 move( 57 path.resolve(npm.prefix, PKGLOCK), 58 path.resolve(npm.prefix, SHRINKWRAP), 59 { Promise: BB } 60 ).then(() => { 61 log.notice('', `${PKGLOCK} has been renamed to ${SHRINKWRAP}. ${SHRINKWRAP} will be used for future installations.`) 62 return readFile(path.resolve(npm.prefix, SHRINKWRAP)).then((d) => { 63 return JSON.parse(d) 64 }) 65 }, (err) => { 66 if (err.code !== 'ENOENT') { 67 throw err 68 } else { 69 return readPackageTree(npm.localPrefix).then( 70 id.computeMetadata 71 ).then((tree) => { 72 return BB.fromNode((cb) => { 73 createShrinkwrap(tree, { 74 silent, 75 defaultFile: SHRINKWRAP 76 }, cb) 77 }) 78 }) 79 } 80 }).then((data) => cb(null, data), cb) 81} 82 83module.exports.createShrinkwrap = createShrinkwrap 84 85function createShrinkwrap (tree, opts, cb) { 86 opts = opts || {} 87 lifecycle(tree.package, 'preshrinkwrap', tree.path, function () { 88 const pkginfo = treeToShrinkwrap(tree) 89 chain([ 90 [lifecycle, tree.package, 'shrinkwrap', tree.path], 91 [shrinkwrap_, tree.path, pkginfo, opts], 92 [lifecycle, tree.package, 'postshrinkwrap', tree.path] 93 ], iferr(cb, function (data) { 94 cb(null, pkginfo) 95 })) 96 }) 97} 98 99function treeToShrinkwrap (tree) { 100 validate('O', arguments) 101 var pkginfo = {} 102 if (tree.package.name) pkginfo.name = tree.package.name 103 if (tree.package.version) pkginfo.version = tree.package.version 104 if (tree.children.length) { 105 pkginfo.requires = true 106 shrinkwrapDeps(pkginfo.dependencies = {}, tree, tree) 107 } 108 return pkginfo 109} 110 111function shrinkwrapDeps (deps, top, tree, seen) { 112 validate('OOO', [deps, top, tree]) 113 if (!seen) seen = new Set() 114 if (seen.has(tree)) return 115 seen.add(tree) 116 sortModules(tree.children).forEach(function (child) { 117 var childIsOnlyDev = isOnlyDev(child) 118 var pkginfo = deps[moduleName(child)] = {} 119 var requested = getRequested(child) || child.package._requested || {} 120 var linked = child.isLink || child.isInLink 121 pkginfo.version = childVersion(top, child, requested) 122 if (requested.type === 'git' && child.package._from) { 123 pkginfo.from = child.package._from 124 } 125 if (child.fromBundle && !linked) { 126 pkginfo.bundled = true 127 } else { 128 if (isRegistry(requested)) { 129 pkginfo.resolved = child.package._resolved 130 } 131 // no integrity for git deps as integrity hashes are based on the 132 // tarball and we can't (yet) create consistent tarballs from a stable 133 // source. 134 if (requested.type !== 'git') { 135 pkginfo.integrity = child.package._integrity || undefined 136 if (!pkginfo.integrity && child.package._shasum) { 137 pkginfo.integrity = ssri.fromHex(child.package._shasum, 'sha1') 138 } 139 } 140 } 141 if (childIsOnlyDev) pkginfo.dev = true 142 if (isOnlyOptional(child)) pkginfo.optional = true 143 if (child.requires.length) { 144 pkginfo.requires = {} 145 sortModules(child.requires).forEach((required) => { 146 var requested = getRequested(required, child) || required.package._requested || {} 147 pkginfo.requires[moduleName(required)] = childRequested(top, required, requested) 148 }) 149 } 150 // iterate into children on non-links and links contained within the top level package 151 if (child.children.length) { 152 pkginfo.dependencies = {} 153 shrinkwrapDeps(pkginfo.dependencies, top, child, seen) 154 } 155 }) 156} 157 158function sortModules (modules) { 159 // sort modules with the locale-agnostic Unicode sort 160 var sortedModuleNames = modules.map(moduleName).sort() 161 return modules.sort((a, b) => ( 162 sortedModuleNames.indexOf(moduleName(a)) - sortedModuleNames.indexOf(moduleName(b)) 163 )) 164} 165 166function childVersion (top, child, req) { 167 if (req.type === 'directory' || req.type === 'file') { 168 return 'file:' + unixFormatPath(path.relative(top.path, child.package._resolved || req.fetchSpec)) 169 } else if (!isRegistry(req) && !child.fromBundle) { 170 return child.package._resolved || req.saveSpec || req.rawSpec 171 } else if (req.type === 'alias') { 172 return `npm:${child.package.name}@${child.package.version}` 173 } else { 174 return child.package.version 175 } 176} 177 178function childRequested (top, child, requested) { 179 if (requested.type === 'directory' || requested.type === 'file') { 180 return 'file:' + unixFormatPath(path.relative(top.path, child.package._resolved || requested.fetchSpec)) 181 } else if (requested.type === 'git' && child.package._from) { 182 return child.package._from 183 } else if (!isRegistry(requested) && !child.fromBundle) { 184 return child.package._resolved || requested.saveSpec || requested.rawSpec 185 } else if (requested.type === 'tag') { 186 // tags are not ranges we can match against, so we invent a "reasonable" 187 // one based on what we actually installed. 188 return npm.config.get('save-prefix') + child.package.version 189 } else if (requested.saveSpec || requested.rawSpec) { 190 return requested.saveSpec || requested.rawSpec 191 } else if (child.package._from || (child.package._requested && child.package._requested.rawSpec)) { 192 return child.package._from.replace(/^@?[^@]+@/, '') || child.package._requested.rawSpec 193 } else { 194 return child.package.version 195 } 196} 197 198function shrinkwrap_ (dir, pkginfo, opts, cb) { 199 save(dir, pkginfo, opts, cb) 200} 201 202function save (dir, pkginfo, opts, cb) { 203 // copy the keys over in a well defined order 204 // because javascript objects serialize arbitrarily 205 BB.join( 206 checkPackageFile(dir, SHRINKWRAP), 207 checkPackageFile(dir, PKGLOCK), 208 checkPackageFile(dir, 'package.json'), 209 (shrinkwrap, lockfile, pkg) => { 210 const info = ( 211 shrinkwrap || 212 lockfile || 213 { 214 path: path.resolve(dir, opts.defaultFile || PKGLOCK), 215 data: '{}', 216 indent: pkg && pkg.indent, 217 newline: pkg && pkg.newline 218 } 219 ) 220 const updated = updateLockfileMetadata(pkginfo, pkg && JSON.parse(pkg.raw)) 221 const swdata = stringifyPackage(updated, info.indent, info.newline) 222 if (swdata === info.raw) { 223 // skip writing if file is identical 224 log.verbose('shrinkwrap', `skipping write for ${path.basename(info.path)} because there were no changes.`) 225 cb(null, pkginfo) 226 } else { 227 inferOwner(info.path).then(owner => { 228 writeFileAtomic(info.path, swdata, (err) => { 229 if (err) return cb(err) 230 if (opts.silent) return cb(null, pkginfo) 231 if (!shrinkwrap && !lockfile) { 232 log.notice('', `created a lockfile as ${path.basename(info.path)}. You should commit this file.`) 233 } 234 if (selfOwner.uid === 0 && (selfOwner.uid !== owner.uid || selfOwner.gid !== owner.gid)) { 235 chown(info.path, owner.uid, owner.gid, er => cb(er, pkginfo)) 236 } else { 237 cb(null, pkginfo) 238 } 239 }) 240 }) 241 } 242 } 243 ).then((file) => { 244 }, cb) 245} 246 247function updateLockfileMetadata (pkginfo, pkgJson) { 248 // This is a lot of work just to make sure the extra metadata fields are 249 // between version and dependencies fields, without affecting any other stuff 250 const newPkg = {} 251 let metainfoWritten = false 252 const metainfo = new Set([ 253 'lockfileVersion', 254 'preserveSymlinks' 255 ]) 256 Object.keys(pkginfo).forEach((k) => { 257 if (k === 'dependencies') { 258 writeMetainfo(newPkg) 259 } 260 if (!metainfo.has(k)) { 261 newPkg[k] = pkginfo[k] 262 } 263 if (k === 'version') { 264 writeMetainfo(newPkg) 265 } 266 }) 267 if (!metainfoWritten) { 268 writeMetainfo(newPkg) 269 } 270 function writeMetainfo (pkginfo) { 271 pkginfo.lockfileVersion = PKGLOCK_VERSION 272 if (process.env.NODE_PRESERVE_SYMLINKS) { 273 pkginfo.preserveSymlinks = process.env.NODE_PRESERVE_SYMLINKS 274 } 275 metainfoWritten = true 276 } 277 return newPkg 278} 279 280function checkPackageFile (dir, name) { 281 const file = path.resolve(dir, name) 282 return readFile( 283 file, 'utf8' 284 ).then((data) => { 285 const format = npm.config.get('format-package-lock') !== false 286 const indent = format ? detectIndent(data).indent : 0 287 const newline = format ? detectNewline(data) : 0 288 289 return { 290 path: file, 291 raw: data, 292 indent, 293 newline 294 } 295 }).catch({code: 'ENOENT'}, () => {}) 296} 297