• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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