publish.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. 'use strict'
  2. const BB = require('bluebird')
  3. const cacache = require('cacache')
  4. const figgyPudding = require('figgy-pudding')
  5. const libpub = require('libnpm/publish')
  6. const libunpub = require('libnpm/unpublish')
  7. const lifecycle = BB.promisify(require('./utils/lifecycle.js'))
  8. const log = require('npmlog')
  9. const npa = require('libnpm/parse-arg')
  10. const npmConfig = require('./config/figgy-config.js')
  11. const output = require('./utils/output.js')
  12. const otplease = require('./utils/otplease.js')
  13. const pack = require('./pack')
  14. const { tarball, extract } = require('libnpm')
  15. const path = require('path')
  16. const readFileAsync = BB.promisify(require('graceful-fs').readFile)
  17. const readJson = BB.promisify(require('read-package-json'))
  18. const semver = require('semver')
  19. const statAsync = BB.promisify(require('graceful-fs').stat)
  20. publish.usage = 'npm publish [<tarball>|<folder>] [--tag <tag>] [--access <public|restricted>] [--dry-run]' +
  21. "\n\nPublishes '.' if no argument supplied" +
  22. '\n\nSets tag `latest` if no --tag specified'
  23. publish.completion = function (opts, cb) {
  24. // publish can complete to a folder with a package.json
  25. // or a tarball, or a tarball url.
  26. // for now, not yet implemented.
  27. return cb()
  28. }
  29. const PublishConfig = figgyPudding({
  30. dryRun: 'dry-run',
  31. 'dry-run': { default: false },
  32. force: { default: false },
  33. json: { default: false },
  34. Promise: { default: () => Promise },
  35. tag: { default: 'latest' },
  36. tmp: {}
  37. })
  38. module.exports = publish
  39. function publish (args, isRetry, cb) {
  40. if (typeof cb !== 'function') {
  41. cb = isRetry
  42. isRetry = false
  43. }
  44. if (args.length === 0) args = ['.']
  45. if (args.length !== 1) return cb(publish.usage)
  46. log.verbose('publish', args)
  47. const opts = PublishConfig(npmConfig())
  48. const t = opts.tag.trim()
  49. if (semver.validRange(t)) {
  50. return cb(new Error('Tag name must not be a valid SemVer range: ' + t))
  51. }
  52. return publish_(args[0], opts)
  53. .then((tarball) => {
  54. const silent = log.level === 'silent'
  55. if (!silent && opts.json) {
  56. output(JSON.stringify(tarball, null, 2))
  57. } else if (!silent) {
  58. output(`+ ${tarball.id}`)
  59. }
  60. })
  61. .nodeify(cb)
  62. }
  63. function publish_ (arg, opts) {
  64. return statAsync(arg).then((stat) => {
  65. if (stat.isDirectory()) {
  66. return stat
  67. } else {
  68. const err = new Error('not a directory')
  69. err.code = 'ENOTDIR'
  70. throw err
  71. }
  72. }).then(() => {
  73. return publishFromDirectory(arg, opts)
  74. }, (err) => {
  75. if (err.code !== 'ENOENT' && err.code !== 'ENOTDIR') {
  76. throw err
  77. } else {
  78. return publishFromPackage(arg, opts)
  79. }
  80. })
  81. }
  82. function publishFromDirectory (arg, opts) {
  83. // All this readJson is because any of the given scripts might modify the
  84. // package.json in question, so we need to refresh after every step.
  85. let contents
  86. return pack.prepareDirectory(arg).then(() => {
  87. return readJson(path.join(arg, 'package.json'))
  88. }).then((pkg) => {
  89. return lifecycle(pkg, 'prepublishOnly', arg)
  90. }).then(() => {
  91. return readJson(path.join(arg, 'package.json'))
  92. }).then((pkg) => {
  93. return cacache.tmp.withTmp(opts.tmp, {tmpPrefix: 'fromDir'}, (tmpDir) => {
  94. const target = path.join(tmpDir, 'package.tgz')
  95. return pack.packDirectory(pkg, arg, target, null, true)
  96. .tap((c) => { contents = c })
  97. .then((c) => !opts.json && pack.logContents(c))
  98. .then(() => upload(pkg, false, target, opts))
  99. })
  100. }).then(() => {
  101. return readJson(path.join(arg, 'package.json'))
  102. }).tap((pkg) => {
  103. return lifecycle(pkg, 'publish', arg)
  104. }).tap((pkg) => {
  105. return lifecycle(pkg, 'postpublish', arg)
  106. })
  107. .then(() => contents)
  108. }
  109. function publishFromPackage (arg, opts) {
  110. return cacache.tmp.withTmp(opts.tmp, {tmpPrefix: 'fromPackage'}, tmp => {
  111. const extracted = path.join(tmp, 'package')
  112. const target = path.join(tmp, 'package.json')
  113. return tarball.toFile(arg, target, opts)
  114. .then(() => extract(arg, extracted, opts))
  115. .then(() => readJson(path.join(extracted, 'package.json')))
  116. .then((pkg) => {
  117. return BB.resolve(pack.getContents(pkg, target))
  118. .tap((c) => !opts.json && pack.logContents(c))
  119. .tap(() => upload(pkg, false, target, opts))
  120. })
  121. })
  122. }
  123. function upload (pkg, isRetry, cached, opts) {
  124. if (!opts.dryRun) {
  125. return readFileAsync(cached).then(tarball => {
  126. return otplease(opts, opts => {
  127. return libpub(pkg, tarball, opts)
  128. }).catch(err => {
  129. if (
  130. err.code === 'EPUBLISHCONFLICT' &&
  131. opts.force &&
  132. !isRetry
  133. ) {
  134. log.warn('publish', 'Forced publish over ' + pkg._id)
  135. return otplease(opts, opts => libunpub(
  136. npa.resolve(pkg.name, pkg.version), opts
  137. )).finally(() => {
  138. // ignore errors. Use the force. Reach out with your feelings.
  139. return otplease(opts, opts => {
  140. return upload(pkg, true, tarball, opts)
  141. }).catch(() => {
  142. // but if it fails again, then report the first error.
  143. throw err
  144. })
  145. })
  146. } else {
  147. throw err
  148. }
  149. })
  150. })
  151. } else {
  152. return opts.Promise.resolve(true)
  153. }
  154. }