version.js 9.8 KB

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