shrinkwrap.js 9.8 KB

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