2019-01-29 14:43:00 -08:00
|
|
|
'use strict'
|
|
|
|
|
|
|
|
const { fixer } = require('normalize-package-data')
|
|
|
|
const npmFetch = require('npm-registry-fetch')
|
2020-10-02 17:52:19 -04:00
|
|
|
const cloneDeep = require('lodash.clonedeep')
|
|
|
|
const npa = require('npm-package-arg')
|
|
|
|
const pack = require('libnpmpack')
|
2019-01-29 14:43:00 -08:00
|
|
|
const semver = require('semver')
|
2020-10-02 17:52:19 -04:00
|
|
|
const { URL } = require('url')
|
|
|
|
const util = require('util')
|
2019-01-29 14:43:00 -08:00
|
|
|
const ssri = require('ssri')
|
|
|
|
|
2020-10-02 17:52:19 -04:00
|
|
|
const statAsync = util.promisify(require('fs').stat)
|
2019-01-29 14:43:00 -08:00
|
|
|
|
|
|
|
module.exports = publish
|
2020-10-02 17:52:19 -04:00
|
|
|
async function publish (folder, manifest, opts) {
|
|
|
|
if (manifest.private) {
|
|
|
|
throw Object.assign(
|
|
|
|
new Error(
|
|
|
|
`This package has been marked as private\n
|
|
|
|
Remove the 'private' field from the package.json to publish it.`
|
|
|
|
),
|
|
|
|
{ code: 'EPRIVATE' }
|
|
|
|
)
|
|
|
|
}
|
2019-01-29 14:43:00 -08:00
|
|
|
|
2020-10-02 17:52:19 -04:00
|
|
|
// spec is used to pick the appropriate registry/auth combo
|
|
|
|
const spec = npa.resolve(manifest.name, manifest.version)
|
|
|
|
opts = {
|
|
|
|
defaultTag: 'latest',
|
|
|
|
// if scoped, restricted by default
|
|
|
|
access: spec.scope ? 'restricted' : 'public',
|
|
|
|
algorithms: ['sha512'],
|
|
|
|
...opts,
|
|
|
|
spec
|
|
|
|
}
|
|
|
|
|
|
|
|
const stat = await statAsync(folder)
|
|
|
|
// checks if it's a dir
|
|
|
|
if (!stat.isDirectory()) {
|
|
|
|
throw Object.assign(
|
|
|
|
new Error('not a directory'),
|
|
|
|
{ code: 'ENOTDIR' }
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
const reg = npmFetch.pickRegistry(spec, opts)
|
|
|
|
const pubManifest = patchManifest(manifest, opts)
|
|
|
|
|
|
|
|
// registry-frontdoor cares about the access level,
|
|
|
|
// which is only configurable for scoped packages
|
|
|
|
if (!spec.scope && opts.access === 'restricted') {
|
|
|
|
throw Object.assign(
|
|
|
|
new Error("Can't restrict access to unscoped packages."),
|
|
|
|
{ code: 'EUNSCOPED' }
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
const tarballData = await pack(`file:${folder}`, { ...opts })
|
|
|
|
const metadata = buildMetadata(reg, pubManifest, tarballData, opts)
|
|
|
|
|
|
|
|
try {
|
|
|
|
return await npmFetch(spec.escapedName, {
|
|
|
|
...opts,
|
|
|
|
method: 'PUT',
|
|
|
|
body: metadata,
|
|
|
|
ignoreBody: true
|
2019-01-29 14:43:00 -08:00
|
|
|
})
|
2020-10-02 17:52:19 -04:00
|
|
|
} catch (err) {
|
|
|
|
if (err.code !== 'E409') { throw err }
|
|
|
|
// if E409, we attempt exactly ONE retry, to protect us
|
|
|
|
// against malicious activity like trying to publish
|
|
|
|
// a bunch of new versions of a package at the same time
|
|
|
|
// and/or spamming the registry
|
|
|
|
const current = await npmFetch.json(spec.escapedName, {
|
|
|
|
...opts,
|
|
|
|
query: { write: true }
|
|
|
|
})
|
|
|
|
const newMetadata = patchMetadata(current, metadata, opts)
|
|
|
|
return npmFetch(spec.escapedName, {
|
|
|
|
...opts,
|
|
|
|
method: 'PUT',
|
|
|
|
body: newMetadata,
|
|
|
|
ignoreBody: true
|
|
|
|
})
|
|
|
|
}
|
2019-01-29 14:43:00 -08:00
|
|
|
}
|
|
|
|
|
2020-10-02 17:52:19 -04:00
|
|
|
function patchManifest (_manifest, opts) {
|
|
|
|
const { npmVersion } = opts
|
|
|
|
const manifest = cloneDeep(_manifest)
|
|
|
|
|
2019-01-29 14:43:00 -08:00
|
|
|
manifest._nodeVersion = process.versions.node
|
2020-10-02 17:52:19 -04:00
|
|
|
if (npmVersion) {
|
|
|
|
manifest._npmVersion = npmVersion
|
2019-01-29 14:43:00 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
fixer.fixNameField(manifest, { strict: true, allowLegacyCase: true })
|
|
|
|
const version = semver.clean(manifest.version)
|
|
|
|
if (!version) {
|
|
|
|
throw Object.assign(
|
|
|
|
new Error('invalid semver: ' + manifest.version),
|
|
|
|
{ code: 'EBADSEMVER' }
|
|
|
|
)
|
|
|
|
}
|
|
|
|
manifest.version = version
|
|
|
|
return manifest
|
|
|
|
}
|
|
|
|
|
2020-10-02 17:52:19 -04:00
|
|
|
function buildMetadata (registry, manifest, tarballData, opts) {
|
|
|
|
const { access, defaultTag, algorithms } = opts
|
2019-01-29 14:43:00 -08:00
|
|
|
const root = {
|
|
|
|
_id: manifest.name,
|
|
|
|
name: manifest.name,
|
|
|
|
description: manifest.description,
|
|
|
|
'dist-tags': {},
|
|
|
|
versions: {},
|
2020-10-02 17:52:19 -04:00
|
|
|
access,
|
2019-01-29 14:43:00 -08:00
|
|
|
readme: manifest.readme || ''
|
|
|
|
}
|
|
|
|
|
2020-10-02 17:52:19 -04:00
|
|
|
root.versions[manifest.version] = manifest
|
|
|
|
const tag = manifest.tag || defaultTag
|
2019-01-29 14:43:00 -08:00
|
|
|
root['dist-tags'][tag] = manifest.version
|
|
|
|
|
2020-10-02 17:52:19 -04:00
|
|
|
const tarballName = `${manifest.name}-${manifest.version}.tgz`
|
|
|
|
const tarballURI = `${manifest.name}/-/${tarballName}`
|
|
|
|
const integrity = ssri.fromData(tarballData, {
|
|
|
|
algorithms: [...new Set(['sha1'].concat(algorithms))]
|
2019-01-29 14:43:00 -08:00
|
|
|
})
|
|
|
|
|
2020-10-02 17:52:19 -04:00
|
|
|
manifest._id = `${manifest.name}@${manifest.version}`
|
|
|
|
manifest.dist = { ...manifest.dist }
|
2019-01-29 14:43:00 -08:00
|
|
|
// Don't bother having sha1 in the actual integrity field
|
2020-10-02 17:52:19 -04:00
|
|
|
manifest.dist.integrity = integrity.sha512[0].toString()
|
2019-01-29 14:43:00 -08:00
|
|
|
// Legacy shasum support
|
2020-10-02 17:52:19 -04:00
|
|
|
manifest.dist.shasum = integrity.sha1[0].hexDigest()
|
|
|
|
|
|
|
|
// NB: the CLI always fetches via HTTPS if the registry is HTTPS,
|
|
|
|
// regardless of what's here. This makes it so that installing
|
|
|
|
// from an HTTP-only mirror doesn't cause problems, though.
|
|
|
|
manifest.dist.tarball = new URL(tarballURI, registry).href
|
2019-01-29 14:43:00 -08:00
|
|
|
.replace(/^https:\/\//, 'http://')
|
|
|
|
|
|
|
|
root._attachments = {}
|
2020-10-02 17:52:19 -04:00
|
|
|
root._attachments[tarballName] = {
|
|
|
|
content_type: 'application/octet-stream',
|
|
|
|
data: tarballData.toString('base64'),
|
|
|
|
length: tarballData.length
|
2019-01-29 14:43:00 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
return root
|
|
|
|
}
|
|
|
|
|
2020-10-02 17:52:19 -04:00
|
|
|
function patchMetadata (current, newData) {
|
2019-01-29 14:43:00 -08:00
|
|
|
const curVers = Object.keys(current.versions || {}).map(v => {
|
|
|
|
return semver.clean(v, true)
|
|
|
|
}).concat(Object.keys(current.time || {}).map(v => {
|
|
|
|
if (semver.valid(v, true)) { return semver.clean(v, true) }
|
|
|
|
})).filter(v => v)
|
|
|
|
|
|
|
|
const newVersion = Object.keys(newData.versions)[0]
|
|
|
|
|
|
|
|
if (curVers.indexOf(newVersion) !== -1) {
|
2020-10-02 17:52:19 -04:00
|
|
|
const { name: pkgid, version } = newData
|
|
|
|
throw Object.assign(
|
|
|
|
new Error(
|
|
|
|
`Cannot publish ${pkgid}@${version} over existing version.`
|
|
|
|
), {
|
|
|
|
code: 'EPUBLISHCONFLICT',
|
|
|
|
pkgid,
|
|
|
|
version
|
|
|
|
})
|
2019-01-29 14:43:00 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
current.versions = current.versions || {}
|
|
|
|
current.versions[newVersion] = newData.versions[newVersion]
|
|
|
|
for (var i in newData) {
|
|
|
|
switch (i) {
|
|
|
|
// objects that copy over the new stuffs
|
|
|
|
case 'dist-tags':
|
|
|
|
case 'versions':
|
|
|
|
case '_attachments':
|
|
|
|
for (var j in newData[i]) {
|
|
|
|
current[i] = current[i] || {}
|
|
|
|
current[i][j] = newData[i][j]
|
|
|
|
}
|
|
|
|
break
|
|
|
|
|
|
|
|
// copy
|
|
|
|
default:
|
|
|
|
current[i] = newData[i]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-02 17:52:19 -04:00
|
|
|
return current
|
2019-01-29 14:43:00 -08:00
|
|
|
}
|