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