publish.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. const util = require('util')
  2. const log = require('npmlog')
  3. const semver = require('semver')
  4. const pack = require('libnpmpack')
  5. const libpub = require('libnpmpublish').publish
  6. const runScript = require('@npmcli/run-script')
  7. const pacote = require('pacote')
  8. const npa = require('npm-package-arg')
  9. const npmFetch = require('npm-registry-fetch')
  10. const chalk = require('chalk')
  11. const replaceInfo = require('./utils/replace-info.js')
  12. const otplease = require('./utils/otplease.js')
  13. const { getContents, logTar } = require('./utils/tar.js')
  14. // for historical reasons, publishConfig in package.json can contain ANY config
  15. // keys that npm supports in .npmrc files and elsewhere. We *may* want to
  16. // revisit this at some point, and have a minimal set that's a SemVer-major
  17. // change that ought to get a RFC written on it.
  18. const flatten = require('./utils/config/flatten.js')
  19. // this is the only case in the CLI where we want to use the old full slow
  20. // 'read-package-json' module, because we want to pull in all the defaults and
  21. // metadata, like git sha's and default scripts and all that.
  22. const readJson = util.promisify(require('read-package-json'))
  23. const BaseCommand = require('./base-command.js')
  24. class Publish extends BaseCommand {
  25. static get description () {
  26. return 'Publish a package'
  27. }
  28. /* istanbul ignore next - see test/lib/load-all-commands.js */
  29. static get name () {
  30. return 'publish'
  31. }
  32. /* istanbul ignore next - see test/lib/load-all-commands.js */
  33. static get params () {
  34. return [
  35. 'tag',
  36. 'access',
  37. 'dry-run',
  38. 'otp',
  39. 'workspace',
  40. 'workspaces',
  41. 'include-workspace-root',
  42. ]
  43. }
  44. /* istanbul ignore next - see test/lib/load-all-commands.js */
  45. static get usage () {
  46. return [
  47. '[<folder>]',
  48. ]
  49. }
  50. exec (args, cb) {
  51. this.publish(args).then(() => cb()).catch(cb)
  52. }
  53. execWorkspaces (args, filters, cb) {
  54. this.publishWorkspaces(args, filters).then(() => cb()).catch(cb)
  55. }
  56. async publish (args) {
  57. if (args.length === 0)
  58. args = ['.']
  59. if (args.length !== 1)
  60. throw this.usageError()
  61. log.verbose('publish', replaceInfo(args))
  62. const unicode = this.npm.config.get('unicode')
  63. const dryRun = this.npm.config.get('dry-run')
  64. const json = this.npm.config.get('json')
  65. const defaultTag = this.npm.config.get('tag')
  66. const ignoreScripts = this.npm.config.get('ignore-scripts')
  67. const silent = log.level === 'silent'
  68. if (semver.validRange(defaultTag))
  69. throw new Error('Tag name must not be a valid SemVer range: ' + defaultTag.trim())
  70. const opts = { ...this.npm.flatOptions }
  71. // you can publish name@version, ./foo.tgz, etc.
  72. // even though the default is the 'file:.' cwd.
  73. const spec = npa(args[0])
  74. let manifest = await this.getManifest(spec, opts)
  75. if (manifest.publishConfig)
  76. flatten(manifest.publishConfig, opts)
  77. // only run scripts for directory type publishes
  78. if (spec.type === 'directory' && !ignoreScripts) {
  79. await runScript({
  80. event: 'prepublishOnly',
  81. path: spec.fetchSpec,
  82. stdio: 'inherit',
  83. pkg: manifest,
  84. banner: !silent,
  85. })
  86. }
  87. const tarballData = await pack(spec, opts)
  88. const pkgContents = await getContents(manifest, tarballData)
  89. // The purpose of re-reading the manifest is in case it changed,
  90. // so that we send the latest and greatest thing to the registry
  91. // note that publishConfig might have changed as well!
  92. manifest = await this.getManifest(spec, opts)
  93. if (manifest.publishConfig)
  94. flatten(manifest.publishConfig, opts)
  95. // note that logTar calls npmlog.notice(), so if we ARE in silent mode,
  96. // this will do nothing, but we still want it in the debuglog if it fails.
  97. if (!json)
  98. logTar(pkgContents, { log, unicode })
  99. if (!dryRun) {
  100. const resolved = npa.resolve(manifest.name, manifest.version)
  101. const registry = npmFetch.pickRegistry(resolved, opts)
  102. const creds = this.npm.config.getCredentialsByURI(registry)
  103. if (!creds.token && !creds.username) {
  104. throw Object.assign(new Error('This command requires you to be logged in.'), {
  105. code: 'ENEEDAUTH',
  106. })
  107. }
  108. await otplease(opts, opts => libpub(manifest, tarballData, opts))
  109. }
  110. if (spec.type === 'directory' && !ignoreScripts) {
  111. await runScript({
  112. event: 'publish',
  113. path: spec.fetchSpec,
  114. stdio: 'inherit',
  115. pkg: manifest,
  116. banner: !silent,
  117. })
  118. await runScript({
  119. event: 'postpublish',
  120. path: spec.fetchSpec,
  121. stdio: 'inherit',
  122. pkg: manifest,
  123. banner: !silent,
  124. })
  125. }
  126. if (!this.suppressOutput) {
  127. if (!silent && json)
  128. this.npm.output(JSON.stringify(pkgContents, null, 2))
  129. else if (!silent)
  130. this.npm.output(`+ ${pkgContents.id}`)
  131. }
  132. return pkgContents
  133. }
  134. async publishWorkspaces (args, filters) {
  135. // Suppresses JSON output in publish() so we can handle it here
  136. this.suppressOutput = true
  137. const results = {}
  138. const json = this.npm.config.get('json')
  139. const silent = log.level === 'silent'
  140. const noop = a => a
  141. const color = this.npm.color ? chalk : { green: noop, bold: noop }
  142. await this.setWorkspaces(filters)
  143. for (const [name, workspace] of this.workspaces.entries()) {
  144. let pkgContents
  145. try {
  146. pkgContents = await this.publish([workspace])
  147. } catch (err) {
  148. if (err.code === 'EPRIVATE') {
  149. log.warn(
  150. 'publish',
  151. `Skipping workspace ${
  152. color.green(name)
  153. }, marked as ${
  154. color.bold('private')
  155. }`
  156. )
  157. continue
  158. }
  159. throw err
  160. }
  161. // This needs to be in-line w/ the rest of the output that non-JSON
  162. // publish generates
  163. if (!silent && !json)
  164. this.npm.output(`+ ${pkgContents.id}`)
  165. else
  166. results[name] = pkgContents
  167. }
  168. if (!silent && json)
  169. this.npm.output(JSON.stringify(results, null, 2))
  170. }
  171. // if it's a directory, read it from the file system
  172. // otherwise, get the full metadata from whatever it is
  173. getManifest (spec, opts) {
  174. if (spec.type === 'directory')
  175. return readJson(`${spec.fetchSpec}/package.json`)
  176. return pacote.manifest(spec, { ...opts, fullMetadata: true })
  177. }
  178. }
  179. module.exports = Publish