UNPKG

9.73 kBJavaScriptView Raw
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) 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 args = args || [ 'commit' ]
290 if (!npm.config.get('commit-hooks')) args.push('-n')
291 return args
292}
293
294function _commit (version, localData, cb) {
295 const options = { env: process.env }
296 const message = npm.config.get('message').replace(/%s/g, version)
297 const signTag = npm.config.get('sign-git-tag')
298 const signCommit = npm.config.get('sign-git-commit')
299 const commitArgs = buildCommitArgs([
300 'commit',
301 ...(signCommit ? ['-S', '-m'] : ['-m']),
302 message
303 ])
304 const flagForTag = signTag ? '-sm' : '-m'
305
306 stagePackageFiles(localData, options).then(() => {
307 return git.exec(commitArgs, options)
308 }).then(() => {
309 if (!localData.existingTag) {
310 return git.exec([
311 'tag', npm.config.get('tag-version-prefix') + version,
312 flagForTag, message
313 ], options)
314 }
315 }).nodeify(cb)
316}
317
318function stagePackageFiles (localData, options) {
319 return addLocalFile('package.json', options, false).then(() => {
320 if (localData.hasShrinkwrap) {
321 return addLocalFile('npm-shrinkwrap.json', options, true)
322 } else if (localData.hasPackageLock) {
323 return addLocalFile('package-lock.json', options, true)
324 }
325 })
326}
327
328function addLocalFile (file, options, ignoreFailure) {
329 const p = git.exec(['add', path.join(npm.localPrefix, file)], options)
330 return ignoreFailure
331 ? p.catch(() => {})
332 : p
333}
334
335function write (data, file, indent, newline, cb) {
336 assert(data && typeof data === 'object', 'must pass data to version write')
337 assert(typeof file === 'string', 'must pass filename to write to version write')
338
339 log.verbose('version.write', 'data', data, 'to', file)
340 writeFileAtomic(
341 path.join(npm.localPrefix, file),
342 stringifyPackage(data, indent, newline),
343 cb
344 )
345}