publish.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. 'use strict'
  2. const cloneDeep = require('lodash.clonedeep')
  3. const figgyPudding = require('figgy-pudding')
  4. const { fixer } = require('normalize-package-data')
  5. const getStream = require('get-stream')
  6. const npa = require('npm-package-arg')
  7. const npmAuth = require('npm-registry-fetch/auth.js')
  8. const npmFetch = require('npm-registry-fetch')
  9. const semver = require('semver')
  10. const ssri = require('ssri')
  11. const url = require('url')
  12. const validate = require('aproba')
  13. const PublishConfig = figgyPudding({
  14. access: {},
  15. algorithms: { default: ['sha512'] },
  16. npmVersion: {},
  17. tag: { default: 'latest' },
  18. Promise: { default: () => Promise }
  19. })
  20. module.exports = publish
  21. function publish (manifest, tarball, opts) {
  22. opts = PublishConfig(opts)
  23. return new opts.Promise(resolve => resolve()).then(() => {
  24. validate('OSO|OOO', [manifest, tarball, opts])
  25. if (manifest.private) {
  26. throw Object.assign(new Error(
  27. 'This package has been marked as private\n' +
  28. "Remove the 'private' field from the package.json to publish it."
  29. ), { code: 'EPRIVATE' })
  30. }
  31. const spec = npa.resolve(manifest.name, manifest.version)
  32. // NOTE: spec is used to pick the appropriate registry/auth combo.
  33. opts = opts.concat(manifest.publishConfig, { spec })
  34. const reg = npmFetch.pickRegistry(spec, opts)
  35. const auth = npmAuth(reg, opts)
  36. const pubManifest = patchedManifest(spec, auth, manifest, opts)
  37. // registry-frontdoor cares about the access level, which is only
  38. // configurable for scoped packages
  39. if (!spec.scope && opts.access === 'restricted') {
  40. throw Object.assign(
  41. new Error("Can't restrict access to unscoped packages."),
  42. { code: 'EUNSCOPED' }
  43. )
  44. }
  45. return slurpTarball(tarball, opts).then(tardata => {
  46. const metadata = buildMetadata(
  47. spec, auth, reg, pubManifest, tardata, opts
  48. )
  49. return npmFetch(spec.escapedName, opts.concat({
  50. method: 'PUT',
  51. body: metadata,
  52. ignoreBody: true
  53. })).catch(err => {
  54. if (err.code !== 'E409') { throw err }
  55. return npmFetch.json(spec.escapedName, opts.concat({
  56. query: { write: true }
  57. })).then(
  58. current => patchMetadata(current, metadata, opts)
  59. ).then(newMetadata => {
  60. return npmFetch(spec.escapedName, opts.concat({
  61. method: 'PUT',
  62. body: newMetadata,
  63. ignoreBody: true
  64. }))
  65. })
  66. })
  67. })
  68. }).then(() => true)
  69. }
  70. function patchedManifest (spec, auth, base, opts) {
  71. const manifest = cloneDeep(base)
  72. manifest._nodeVersion = process.versions.node
  73. if (opts.npmVersion) {
  74. manifest._npmVersion = opts.npmVersion
  75. }
  76. if (auth.username || auth.email) {
  77. // NOTE: This is basically pointless, but reproduced because it's what
  78. // legacy does: tl;dr `auth.username` and `auth.email` are going to be
  79. // undefined in any auth situation that uses tokens instead of plain
  80. // auth. I can only assume some registries out there decided that
  81. // _npmUser would be of any use to them, but _npmUser in packuments
  82. // currently gets filled in by the npm registry itself, based on auth
  83. // information.
  84. manifest._npmUser = {
  85. name: auth.username,
  86. email: auth.email
  87. }
  88. }
  89. fixer.fixNameField(manifest, { strict: true, allowLegacyCase: true })
  90. const version = semver.clean(manifest.version)
  91. if (!version) {
  92. throw Object.assign(
  93. new Error('invalid semver: ' + manifest.version),
  94. { code: 'EBADSEMVER' }
  95. )
  96. }
  97. manifest.version = version
  98. return manifest
  99. }
  100. function buildMetadata (spec, auth, registry, manifest, tardata, opts) {
  101. const root = {
  102. _id: manifest.name,
  103. name: manifest.name,
  104. description: manifest.description,
  105. 'dist-tags': {},
  106. versions: {},
  107. readme: manifest.readme || ''
  108. }
  109. if (opts.access) root.access = opts.access
  110. if (!auth.token) {
  111. root.maintainers = [{ name: auth.username, email: auth.email }]
  112. manifest.maintainers = JSON.parse(JSON.stringify(root.maintainers))
  113. }
  114. root.versions[ manifest.version ] = manifest
  115. const tag = manifest.tag || opts.tag
  116. root['dist-tags'][tag] = manifest.version
  117. const tbName = manifest.name + '-' + manifest.version + '.tgz'
  118. const tbURI = manifest.name + '/-/' + tbName
  119. const integrity = ssri.fromData(tardata, {
  120. algorithms: [...new Set(['sha1'].concat(opts.algorithms))]
  121. })
  122. manifest._id = manifest.name + '@' + manifest.version
  123. manifest.dist = manifest.dist || {}
  124. // Don't bother having sha1 in the actual integrity field
  125. manifest.dist.integrity = integrity['sha512'][0].toString()
  126. // Legacy shasum support
  127. manifest.dist.shasum = integrity['sha1'][0].hexDigest()
  128. manifest.dist.tarball = url.resolve(registry, tbURI)
  129. .replace(/^https:\/\//, 'http://')
  130. root._attachments = {}
  131. root._attachments[ tbName ] = {
  132. 'content_type': 'application/octet-stream',
  133. 'data': tardata.toString('base64'),
  134. 'length': tardata.length
  135. }
  136. return root
  137. }
  138. function patchMetadata (current, newData, opts) {
  139. const curVers = Object.keys(current.versions || {}).map(v => {
  140. return semver.clean(v, true)
  141. }).concat(Object.keys(current.time || {}).map(v => {
  142. if (semver.valid(v, true)) { return semver.clean(v, true) }
  143. })).filter(v => v)
  144. const newVersion = Object.keys(newData.versions)[0]
  145. if (curVers.indexOf(newVersion) !== -1) {
  146. throw ConflictError(newData.name, newData.version)
  147. }
  148. current.versions = current.versions || {}
  149. current.versions[newVersion] = newData.versions[newVersion]
  150. for (var i in newData) {
  151. switch (i) {
  152. // objects that copy over the new stuffs
  153. case 'dist-tags':
  154. case 'versions':
  155. case '_attachments':
  156. for (var j in newData[i]) {
  157. current[i] = current[i] || {}
  158. current[i][j] = newData[i][j]
  159. }
  160. break
  161. // ignore these
  162. case 'maintainers':
  163. break
  164. // copy
  165. default:
  166. current[i] = newData[i]
  167. }
  168. }
  169. const maint = newData.maintainers && JSON.parse(JSON.stringify(newData.maintainers))
  170. newData.versions[newVersion].maintainers = maint
  171. return current
  172. }
  173. function slurpTarball (tarSrc, opts) {
  174. if (Buffer.isBuffer(tarSrc)) {
  175. return opts.Promise.resolve(tarSrc)
  176. } else if (typeof tarSrc === 'string') {
  177. return opts.Promise.resolve(Buffer.from(tarSrc, 'base64'))
  178. } else if (typeof tarSrc.pipe === 'function') {
  179. return getStream.buffer(tarSrc)
  180. } else {
  181. return opts.Promise.reject(Object.assign(
  182. new Error('invalid tarball argument. Must be a Buffer, a base64 string, or a binary stream'), {
  183. code: 'EBADTAR'
  184. }))
  185. }
  186. }
  187. function ConflictError (pkgid, version) {
  188. return Object.assign(new Error(
  189. `Cannot publish ${pkgid}@${version} over existing version.`
  190. ), {
  191. code: 'EPUBLISHCONFLICT',
  192. pkgid,
  193. version
  194. })
  195. }