This repository has been archived by the owner on Jan 19, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(publish): add initial publish support. tests tbd
- Loading branch information
Showing
1 changed file
with
201 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,202 @@ | ||
'use strict' | ||
|
||
const cloneDeep = require('lodash.clonedeep') | ||
const figgyPudding = require('figgy-pudding') | ||
const { fixer } = require('normalize-package-data') | ||
const getStream = require('get-stream') | ||
const npa = require('npm-package-arg') | ||
const npmAuth = require('npm-registry-fetch/auth.js') | ||
const npmFetch = require('npm-registry-fetch') | ||
const semver = require('semver') | ||
const ssri = require('ssri') | ||
const url = require('url') | ||
const validate = require('aproba') | ||
|
||
const PublishConfig = figgyPudding({ | ||
access: { default: 'restricted' }, | ||
integrityHashes: { default: ['sha512'] }, | ||
dryRun: 'dry-run', | ||
'dry-run': {}, | ||
force: {}, | ||
npmVersion: {}, | ||
Promise: { default: () => Promise } | ||
}) | ||
|
||
module.exports = publish | ||
function publish (manifest, tarball, opts) { | ||
opts = PublishConfig(opts) | ||
return new opts.Promise(resolve => resolve()).then(() => { | ||
validate('OSO|OOO', [manifest, tarball, opts]) | ||
if (manifest.private) { | ||
throw new Error( | ||
'This package has been marked as private\n' + | ||
"Remove the 'private' field from the package.json to publish it." | ||
) | ||
} | ||
const spec = npa.resolve(manifest.name, manifest.version) | ||
const reg = npmFetch.pickRegistry(spec, opts) | ||
const auth = npmAuth(reg, opts) | ||
const pubManifest = patchedManifest(spec, auth, manifest, opts) | ||
|
||
// registry-frontdoor cares about the access level, which is only | ||
// configurable for scoped packages | ||
if (!spec.scope && opts.access === 'restricted') { | ||
throw new Error("Can't restrict access to unscoped packages.") | ||
} | ||
|
||
return slurpTarball(tarball).then(tardata => { | ||
const metadata = buildMetadata( | ||
spec, auth, reg, pubManifest, tardata, opts | ||
) | ||
return npmFetch.json(spec.escapedName, opts.concat({ | ||
method: 'PUT', | ||
body: metadata | ||
})).catch(err => { | ||
if (err.code !== 'E409') { throw err } | ||
return npmFetch.json(spec.escapedName, opts.concat({ | ||
query: { write: true } | ||
})).then( | ||
current => patchMetadata(current, metadata, opts) | ||
).then(newMetadata => { | ||
return npmFetch.json(spec.escapedName, opts.concat({ | ||
method: 'PUT', | ||
body: newMetadata | ||
})) | ||
}) | ||
}) | ||
}) | ||
}) | ||
} | ||
|
||
function patchedManifest (spec, auth, base, opts) { | ||
const manifest = cloneDeep(base) | ||
manifest._nodeVersion = process.versions.node | ||
if (opts.npmVersion) { | ||
manifest._npmVersion = opts.npmVersion | ||
} | ||
if (auth.username || auth.email) { | ||
// NOTE: This is basically pointless, but reproduced because it's what | ||
// legacy does: tl;dr `auth.username` and `auth.email` are going to be | ||
// undefined in any auth situation that uses tokens instead of plain | ||
// auth. I can only assume some registries out there decided that | ||
// _npmUser would be of any use to them, but _npmUser in packuments | ||
// currently gets filled in by the npm registry itself, based on auth | ||
// information. | ||
manifest._npmUser = { | ||
username: auth.username, | ||
email: auth.email | ||
} | ||
} | ||
|
||
fixer.fixNameField(manifest, { strict: true, allowLegacyCase: true }) | ||
const version = semver.clean(manifest.version) | ||
if (!version) { throw new Error('invalid semver: ' + manifest.version) } | ||
manifest.version = version | ||
return manifest | ||
} | ||
|
||
function buildMetadata (spec, auth, registry, manifest, tardata, opts) { | ||
const root = { | ||
_id: manifest.name, | ||
name: manifest.name, | ||
description: manifest.description, | ||
'dist-tags': {}, | ||
versions: {}, | ||
readme: manifest.readme || '' | ||
} | ||
|
||
if (opts.access) root.access = opts.access | ||
|
||
if (!auth.token) { | ||
root.maintainers = [{ name: auth.username, email: auth.email }] | ||
manifest.maintainers = JSON.parse(JSON.stringify(root.maintainers)) | ||
} | ||
|
||
root.versions[ manifest.version ] = manifest | ||
const tag = manifest.tag || this.config.defaultTag | ||
root['dist-tags'][tag] = manifest.version | ||
|
||
const tbName = manifest.name + '-' + manifest.version + '.tgz' | ||
const tbURI = manifest.name + '/-/' + tbName | ||
const integrity = ssri.fromData(tardata, { | ||
algorithms: [...new Set(['sha1'].concat(opts.integrityHashes))] | ||
}) | ||
|
||
manifest._id = manifest.name + '@' + manifest.version | ||
manifest.dist = manifest.dist || {} | ||
// Don't bother having sha1 in the actual integrity field | ||
manifest.dist.integrity = integrity['sha512'][0].toString() | ||
// Legacy shasum support | ||
manifest.dist.shasum = integrity['sha1'][0].hexDigest() | ||
manifest.dist.tarball = url.resolve(registry, tbURI) | ||
.replace(/^https:\/\//, 'http://') | ||
|
||
root._attachments = {} | ||
root._attachments[ tbName ] = { | ||
'content_type': 'application/octet-stream', | ||
'data': tardata.toString('base64'), | ||
'length': tardata.length | ||
} | ||
|
||
return root | ||
} | ||
|
||
function patchMetadata (current, newData, opts) { | ||
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)) | ||
|
||
if (curVers.indexOf(newData.version) !== -1) { | ||
throw ConflictError(newData.name, newData.version) | ||
} | ||
|
||
const newVersion = newData.version | ||
|
||
current.versions[newVersion] = newData.versions[newVersion] | ||
current._attachments = current._attachments || {} | ||
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][j] = newData[i][j] | ||
} | ||
break | ||
|
||
// ignore these | ||
case 'maintainers': | ||
break | ||
|
||
// copy | ||
default: | ||
current[i] = newData[i] | ||
} | ||
} | ||
const maint = JSON.parse(JSON.stringify(newData.maintainers)) | ||
newData.versions[newVersion].maintainers = maint | ||
return current | ||
} | ||
|
||
function slurpTarball (tarSrc, opts) { | ||
if (Buffer.isBuffer(tarSrc)) { | ||
return opts.Promise.resolve(tarSrc) | ||
} else if (typeof tarSrc === 'string') { | ||
return opts.Promise.resolve(Buffer.from(tarSrc, 'base64')) | ||
} else if (typeof tarSrc === 'object' && typeof tarSrc.pipe === 'function') { | ||
return getStream.buffer(tarSrc) | ||
} | ||
} | ||
|
||
function ConflictError (pkgid, version) { | ||
return Object.assign(new Error( | ||
`Cannot publish ${pkgid}@${version} over existing version.` | ||
), { | ||
code: 'EPUBLISHCONFLICT', | ||
pkgid, | ||
version | ||
}) | ||
} |