• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict'
2const BB = require('bluebird')
3
4const assert = require('assert')
5const chain = require('slide').chain
6const detectIndent = require('detect-indent')
7const detectNewline = require('detect-newline')
8const fs = require('graceful-fs')
9const readFile = BB.promisify(require('graceful-fs').readFile)
10const git = require('./utils/git.js')
11const lifecycle = require('./utils/lifecycle.js')
12const log = require('npmlog')
13const npm = require('./npm.js')
14const output = require('./utils/output.js')
15const parseJSON = require('./utils/parse-json.js')
16const path = require('path')
17const semver = require('semver')
18const stringifyPackage = require('stringify-package')
19const writeFileAtomic = require('write-file-atomic')
20
21version.usage = 'npm version [<newversion> | major | minor | patch | premajor | preminor | prepatch | prerelease [--preid=<prerelease-id>] | from-git]' +
22                '\n(run in package dir)\n' +
23                "'npm -v' or 'npm --version' to print npm version " +
24                '(' + npm.version + ')\n' +
25                "'npm view <pkg> version' to view a package's " +
26                'published version\n' +
27                "'npm ls' to inspect current package/dependency versions"
28
29// npm version <newver>
30module.exports = version
31function version (args, silent, cb_) {
32  if (typeof cb_ !== 'function') {
33    cb_ = silent
34    silent = false
35  }
36  if (args.length > 1) return cb_(version.usage)
37
38  readPackage(function (er, data, indent, newline) {
39    if (!args.length) return dump(data, cb_)
40
41    if (er) {
42      log.error('version', 'No valid package.json found')
43      return cb_(er)
44    }
45
46    if (args[0] === 'from-git') {
47      retrieveTagVersion(silent, data, cb_)
48    } else {
49      var newVersion = semver.valid(args[0])
50      if (!newVersion) newVersion = semver.inc(data.version, args[0], npm.config.get('preid'))
51      if (!newVersion) return cb_(version.usage)
52      persistVersion(newVersion, silent, data, cb_)
53    }
54  })
55}
56
57function retrieveTagVersion (silent, data, cb_) {
58  chain([
59    verifyGit,
60    parseLastGitTag
61  ], function (er, results) {
62    if (er) return cb_(er)
63    var localData = {
64      hasGit: true,
65      existingTag: true
66    }
67
68    var version = results[results.length - 1]
69    persistVersion(version, silent, data, localData, cb_)
70  })
71}
72
73function parseLastGitTag (cb) {
74  var options = { env: process.env }
75  git.whichAndExec(['describe', '--abbrev=0'], options, function (er, stdout) {
76    if (er) {
77      if (er.message.indexOf('No names found') !== -1) return cb(new Error('No tags found'))
78      return cb(er)
79    }
80
81    var tag = stdout.trim()
82    var prefix = npm.config.get('tag-version-prefix')
83    // Strip the prefix from the start of the tag:
84    if (tag.indexOf(prefix) === 0) tag = tag.slice(prefix.length)
85    var version = semver.valid(tag)
86    if (!version) return cb(new Error(tag + ' is not a valid version'))
87    cb(null, version)
88  })
89}
90
91function persistVersion (newVersion, silent, data, localData, cb_) {
92  if (typeof localData === 'function') {
93    cb_ = localData
94    localData = {}
95  }
96
97  if (!npm.config.get('allow-same-version') && data.version === newVersion) {
98    return cb_(new Error('Version not changed, might want --allow-same-version'))
99  }
100  data.version = newVersion
101  var lifecycleData = Object.create(data)
102  lifecycleData._id = data.name + '@' + newVersion
103
104  var where = npm.prefix
105  chain([
106    !localData.hasGit && [checkGit, localData],
107    [lifecycle, lifecycleData, 'preversion', where],
108    [updatePackage, newVersion, silent],
109    [lifecycle, lifecycleData, 'version', where],
110    [commit, localData, newVersion],
111    [lifecycle, lifecycleData, 'postversion', where]
112  ], cb_)
113}
114
115function readPackage (cb) {
116  var packagePath = path.join(npm.localPrefix, 'package.json')
117  fs.readFile(packagePath, 'utf8', function (er, data) {
118    if (er) return cb(new Error(er))
119    var indent
120    var newline
121    try {
122      indent = detectIndent(data).indent
123      newline = detectNewline(data)
124      data = JSON.parse(data)
125    } catch (e) {
126      er = e
127      data = null
128    }
129    cb(er, data, indent, newline)
130  })
131}
132
133function updatePackage (newVersion, silent, cb_) {
134  function cb (er) {
135    if (!er && !silent) output('v' + newVersion)
136    cb_(er)
137  }
138
139  readPackage(function (er, data, indent, newline) {
140    if (er) return cb(new Error(er))
141    data.version = newVersion
142    write(data, 'package.json', indent, newline, cb)
143  })
144}
145
146function commit (localData, newVersion, cb) {
147  updateShrinkwrap(newVersion, function (er, hasShrinkwrap, hasLock) {
148    if (er || !localData.hasGit) return cb(er)
149    localData.hasShrinkwrap = hasShrinkwrap
150    localData.hasPackageLock = hasLock
151    _commit(newVersion, localData, cb)
152  })
153}
154
155const SHRINKWRAP = 'npm-shrinkwrap.json'
156const PKGLOCK = 'package-lock.json'
157
158function readLockfile (name) {
159  return readFile(
160    path.join(npm.localPrefix, name), 'utf8'
161  ).catch({code: 'ENOENT'}, () => null)
162}
163
164function updateShrinkwrap (newVersion, cb) {
165  BB.join(
166    readLockfile(SHRINKWRAP),
167    readLockfile(PKGLOCK),
168    (shrinkwrap, lockfile) => {
169      if (!shrinkwrap && !lockfile) {
170        return cb(null, false, false)
171      }
172      const file = shrinkwrap ? SHRINKWRAP : PKGLOCK
173      let data
174      let indent
175      let newline
176      try {
177        data = parseJSON(shrinkwrap || lockfile)
178        indent = detectIndent(shrinkwrap || lockfile).indent
179        newline = detectNewline(shrinkwrap || lockfile)
180      } catch (err) {
181        log.error('version', `Bad ${file} data.`)
182        return cb(err)
183      }
184      data.version = newVersion
185      write(data, file, indent, newline, (err) => {
186        if (err) {
187          log.error('version', `Failed to update version in ${file}`)
188          return cb(err)
189        } else {
190          return cb(null, !!shrinkwrap, !!lockfile)
191        }
192      })
193    }
194  )
195}
196
197function dump (data, cb) {
198  var v = {}
199
200  if (data && data.name && data.version) v[data.name] = data.version
201  v.npm = npm.version
202  Object.keys(process.versions).sort().forEach(function (k) {
203    v[k] = process.versions[k]
204  })
205
206  if (npm.config.get('json')) v = JSON.stringify(v, null, 2)
207
208  output(v)
209  cb()
210}
211
212function statGitFolder (cb) {
213  fs.stat(path.join(npm.localPrefix, '.git'), cb)
214}
215
216function callGitStatus (cb) {
217  git.whichAndExec(
218    [ 'status', '--porcelain' ],
219    { env: process.env },
220    cb
221  )
222}
223
224function cleanStatusLines (stdout) {
225  var lines = stdout.trim().split('\n').filter(function (line) {
226    return line.trim() && !line.match(/^\?\? /)
227  }).map(function (line) {
228    return line.trim()
229  })
230
231  return lines
232}
233
234function verifyGit (cb) {
235  function checkStatus (er) {
236    if (er) return cb(er)
237    callGitStatus(checkStdout)
238  }
239
240  function checkStdout (er, stdout) {
241    if (er) return cb(er)
242    var lines = cleanStatusLines(stdout)
243    if (lines.length > 0) {
244      return cb(new Error(
245        'Git working directory not clean.\n' + lines.join('\n')
246      ))
247    }
248
249    cb()
250  }
251
252  statGitFolder(checkStatus)
253}
254
255function checkGit (localData, cb) {
256  statGitFolder(function (er) {
257    var doGit = !er && npm.config.get('git-tag-version')
258    if (!doGit) {
259      if (er && npm.config.get('git-tag-version')) log.verbose('version', 'error checking for .git', er)
260      log.verbose('version', 'not tagging in git')
261      return cb(null, false)
262    }
263
264    // check for git
265    callGitStatus(function (er, stdout) {
266      if (er && er.code === 'ENOGIT') {
267        log.warn(
268          'version',
269          'This is a Git checkout, but the git command was not found.',
270          'npm could not create a Git tag for this release!'
271        )
272        return cb(null, false)
273      }
274
275      var lines = cleanStatusLines(stdout)
276      if (lines.length && !npm.config.get('force')) {
277        return cb(new Error(
278          'Git working directory not clean.\n' + lines.join('\n')
279        ))
280      }
281      localData.hasGit = true
282      cb(null, true)
283    })
284  })
285}
286
287module.exports.buildCommitArgs = buildCommitArgs
288function buildCommitArgs (args) {
289  const add = []
290  args = args || []
291  if (args[0] === 'commit') args.shift()
292  if (!npm.config.get('commit-hooks')) add.push('-n')
293  if (npm.config.get('allow-same-version')) add.push('--allow-empty')
294  return ['commit', ...add, ...args]
295}
296
297module.exports.buildTagFlags = buildTagFlags
298function buildTagFlags () {
299  return '-'.concat(
300    npm.config.get('sign-git-tag') ? 's' : '',
301    npm.config.get('allow-same-version') ? 'f' : '',
302    'm'
303  )
304}
305
306function _commit (version, localData, cb) {
307  const options = { env: process.env }
308  const message = npm.config.get('message').replace(/%s/g, version)
309  const signCommit = npm.config.get('sign-git-commit')
310  const commitArgs = buildCommitArgs([
311    'commit',
312    ...(signCommit ? ['-S', '-m'] : ['-m']),
313    message
314  ])
315
316  stagePackageFiles(localData, options).then(() => {
317    return git.exec(commitArgs, options)
318  }).then(() => {
319    if (!localData.existingTag) {
320      return git.exec([
321        'tag', npm.config.get('tag-version-prefix') + version,
322        buildTagFlags(), message
323      ], options)
324    }
325  }).nodeify(cb)
326}
327
328function stagePackageFiles (localData, options) {
329  return addLocalFile('package.json', options, false).then(() => {
330    if (localData.hasShrinkwrap) {
331      return addLocalFile('npm-shrinkwrap.json', options, true)
332    } else if (localData.hasPackageLock) {
333      return addLocalFile('package-lock.json', options, true)
334    }
335  })
336}
337
338function addLocalFile (file, options, ignoreFailure) {
339  const p = git.exec(['add', path.join(npm.localPrefix, file)], options)
340  return ignoreFailure
341    ? p.catch(() => {})
342    : p
343}
344
345function write (data, file, indent, newline, cb) {
346  assert(data && typeof data === 'object', 'must pass data to version write')
347  assert(typeof file === 'string', 'must pass filename to write to version write')
348
349  log.verbose('version.write', 'data', data, 'to', file)
350  writeFileAtomic(
351    path.join(npm.localPrefix, file),
352    stringifyPackage(data, indent, newline),
353    cb
354  )
355}
356