/lib/version.js

http://github.com/isaacs/npm · JavaScript · 289 lines · 247 code · 39 blank · 3 comment · 46 complexity · 30b4a2f7861f4d30b949771c80f97499 MD5 · raw file

  1. // npm version <newver>
  2. module.exports = version
  3. var semver = require('semver')
  4. var path = require('path')
  5. var fs = require('graceful-fs')
  6. var writeFileAtomic = require('write-file-atomic')
  7. var chain = require('slide').chain
  8. var log = require('npmlog')
  9. var npm = require('./npm.js')
  10. var git = require('./utils/git.js')
  11. var assert = require('assert')
  12. var lifecycle = require('./utils/lifecycle.js')
  13. var parseJSON = require('./utils/parse-json.js')
  14. var output = require('./utils/output.js')
  15. version.usage = 'npm version [<newversion> | major | minor | patch | premajor | preminor | prepatch | prerelease | from-git]' +
  16. '\n(run in package dir)\n' +
  17. "'npm -v' or 'npm --version' to print npm version " +
  18. '(' + npm.version + ')\n' +
  19. "'npm view <pkg> version' to view a package's " +
  20. 'published version\n' +
  21. "'npm ls' to inspect current package/dependency versions"
  22. function version (args, silent, cb_) {
  23. if (typeof cb_ !== 'function') {
  24. cb_ = silent
  25. silent = false
  26. }
  27. if (args.length > 1) return cb_(version.usage)
  28. readPackage(function (er, data) {
  29. if (!args.length) return dump(data, cb_)
  30. if (er) {
  31. log.error('version', 'No valid package.json found')
  32. return cb_(er)
  33. }
  34. if (args[0] === 'from-git') {
  35. retrieveTagVersion(silent, data, cb_)
  36. } else {
  37. var newVersion = semver.valid(args[0])
  38. if (!newVersion) newVersion = semver.inc(data.version, args[0])
  39. if (!newVersion) return cb_(version.usage)
  40. persistVersion(newVersion, silent, data, cb_)
  41. }
  42. })
  43. }
  44. function retrieveTagVersion (silent, data, cb_) {
  45. chain([
  46. verifyGit,
  47. parseLastGitTag
  48. ], function (er, results) {
  49. if (er) return cb_(er)
  50. var localData = {
  51. hasGit: true,
  52. existingTag: true
  53. }
  54. var version = results[results.length - 1]
  55. persistVersion(version, silent, data, localData, cb_)
  56. })
  57. }
  58. function parseLastGitTag (cb) {
  59. var options = { env: process.env }
  60. git.whichAndExec(['describe', '--abbrev=0'], options, function (er, stdout) {
  61. if (er) {
  62. if (er.message.indexOf('No names found') !== -1) return cb(new Error('No tags found'))
  63. return cb(er)
  64. }
  65. var tag = stdout.trim()
  66. var prefix = npm.config.get('tag-version-prefix')
  67. // Strip the prefix from the start of the tag:
  68. if (tag.indexOf(prefix) === 0) tag = tag.slice(prefix.length)
  69. var version = semver.valid(tag)
  70. if (!version) return cb(new Error(tag + ' is not a valid version'))
  71. cb(null, version)
  72. })
  73. }
  74. function persistVersion (newVersion, silent, data, localData, cb_) {
  75. if (typeof localData === 'function') {
  76. cb_ = localData
  77. localData = {}
  78. }
  79. if (data.version === newVersion) return cb_(new Error('Version not changed'))
  80. data.version = newVersion
  81. var lifecycleData = Object.create(data)
  82. lifecycleData._id = data.name + '@' + newVersion
  83. var where = npm.prefix
  84. chain([
  85. !localData.hasGit && [checkGit, localData],
  86. [lifecycle, lifecycleData, 'preversion', where],
  87. [updatePackage, newVersion, silent],
  88. [lifecycle, lifecycleData, 'version', where],
  89. [commit, localData, newVersion],
  90. [lifecycle, lifecycleData, 'postversion', where]
  91. ], cb_)
  92. }
  93. function readPackage (cb) {
  94. var packagePath = path.join(npm.localPrefix, 'package.json')
  95. fs.readFile(packagePath, function (er, data) {
  96. if (er) return cb(new Error(er))
  97. if (data) data = data.toString()
  98. try {
  99. data = JSON.parse(data)
  100. } catch (e) {
  101. er = e
  102. data = null
  103. }
  104. cb(er, data)
  105. })
  106. }
  107. function updatePackage (newVersion, silent, cb_) {
  108. function cb (er) {
  109. if (!er && !silent) output('v' + newVersion)
  110. cb_(er)
  111. }
  112. readPackage(function (er, data) {
  113. if (er) return cb(new Error(er))
  114. data.version = newVersion
  115. write(data, 'package.json', cb)
  116. })
  117. }
  118. function commit (localData, newVersion, cb) {
  119. updateShrinkwrap(newVersion, function (er, hasShrinkwrap) {
  120. if (er || !localData.hasGit) return cb(er)
  121. localData.hasShrinkwrap = hasShrinkwrap
  122. _commit(newVersion, localData, cb)
  123. })
  124. }
  125. function updateShrinkwrap (newVersion, cb) {
  126. fs.readFile(path.join(npm.localPrefix, 'npm-shrinkwrap.json'), function (er, data) {
  127. if (er && er.code === 'ENOENT') return cb(null, false)
  128. try {
  129. data = data.toString()
  130. data = parseJSON(data)
  131. } catch (er) {
  132. log.error('version', 'Bad npm-shrinkwrap.json data')
  133. return cb(er)
  134. }
  135. data.version = newVersion
  136. write(data, 'npm-shrinkwrap.json', function (er) {
  137. if (er) {
  138. log.error('version', 'Bad npm-shrinkwrap.json data')
  139. return cb(er)
  140. }
  141. cb(null, true)
  142. })
  143. })
  144. }
  145. function dump (data, cb) {
  146. var v = {}
  147. if (data && data.name && data.version) v[data.name] = data.version
  148. v.npm = npm.version
  149. Object.keys(process.versions).sort().forEach(function (k) {
  150. v[k] = process.versions[k]
  151. })
  152. if (npm.config.get('json')) v = JSON.stringify(v, null, 2)
  153. output(v)
  154. cb()
  155. }
  156. function statGitFolder (cb) {
  157. fs.stat(path.join(npm.localPrefix, '.git'), cb)
  158. }
  159. function callGitStatus (cb) {
  160. git.whichAndExec(
  161. [ 'status', '--porcelain' ],
  162. { env: process.env },
  163. cb
  164. )
  165. }
  166. function cleanStatusLines (stdout) {
  167. var lines = stdout.trim().split('\n').filter(function (line) {
  168. return line.trim() && !line.match(/^\?\? /)
  169. }).map(function (line) {
  170. return line.trim()
  171. })
  172. return lines
  173. }
  174. function verifyGit (cb) {
  175. function checkStatus (er) {
  176. if (er) return cb(er)
  177. callGitStatus(checkStdout)
  178. }
  179. function checkStdout (er, stdout) {
  180. if (er) return cb(er)
  181. var lines = cleanStatusLines(stdout)
  182. if (lines.length > 0) {
  183. return cb(new Error(
  184. 'Git working directory not clean.\n' + lines.join('\n')
  185. ))
  186. }
  187. cb()
  188. }
  189. statGitFolder(checkStatus)
  190. }
  191. function checkGit (localData, cb) {
  192. statGitFolder(function (er) {
  193. var doGit = !er && npm.config.get('git-tag-version')
  194. if (!doGit) {
  195. if (er) log.verbose('version', 'error checking for .git', er)
  196. log.verbose('version', 'not tagging in git')
  197. return cb(null, false)
  198. }
  199. // check for git
  200. callGitStatus(function (er, stdout) {
  201. if (er && er.code === 'ENOGIT') {
  202. log.warn(
  203. 'version',
  204. 'This is a Git checkout, but the git command was not found.',
  205. 'npm could not create a Git tag for this release!'
  206. )
  207. return cb(null, false)
  208. }
  209. var lines = cleanStatusLines(stdout)
  210. if (lines.length && !npm.config.get('force')) {
  211. return cb(new Error(
  212. 'Git working directory not clean.\n' + lines.join('\n')
  213. ))
  214. }
  215. localData.hasGit = true
  216. cb(null, true)
  217. })
  218. })
  219. }
  220. function _commit (version, localData, cb) {
  221. var packagePath = path.join(npm.localPrefix, 'package.json')
  222. var options = { env: process.env }
  223. var message = npm.config.get('message').replace(/%s/g, version)
  224. var sign = npm.config.get('sign-git-tag')
  225. var flag = sign ? '-sm' : '-am'
  226. chain(
  227. [
  228. git.chainableExec([ 'add', packagePath ], options),
  229. localData.hasShrinkwrap && git.chainableExec([ 'add', 'npm-shrinkwrap.json' ], options),
  230. git.chainableExec([ 'commit', '-m', message ], options),
  231. !localData.existingTag && git.chainableExec([
  232. 'tag',
  233. npm.config.get('tag-version-prefix') + version,
  234. flag,
  235. message
  236. ], options)
  237. ],
  238. cb
  239. )
  240. }
  241. function write (data, file, cb) {
  242. assert(data && typeof data === 'object', 'must pass data to version write')
  243. assert(typeof file === 'string', 'must pass filename to write to version write')
  244. log.verbose('version.write', 'data', data, 'to', file)
  245. writeFileAtomic(
  246. path.join(npm.localPrefix, file),
  247. new Buffer(JSON.stringify(data, null, 2) + '\n'),
  248. cb
  249. )
  250. }