diff --git a/lib/config/figgy-config.js b/lib/config/figgy-config.js index b50370330368a..05a88ffd2f3f1 100644 --- a/lib/config/figgy-config.js +++ b/lib/config/figgy-config.js @@ -36,6 +36,8 @@ function mkConfig (...providers) { dmode: npm.modes.exec, fmode: npm.modes.file, umask: npm.modes.umask, + npmVersion: npm.version, + tmp: npm.tmp, Promise: BB }) const ownerStats = calculateOwner() diff --git a/lib/publish.js b/lib/publish.js index 25f2134b1b16d..7d93615a2c167 100644 --- a/lib/publish.js +++ b/lib/publish.js @@ -3,20 +3,21 @@ const BB = require('bluebird') const cacache = require('cacache') -const createReadStream = require('graceful-fs').createReadStream -const getPublishConfig = require('./utils/get-publish-config.js') +const figgyPudding = require('figgy-pudding') +const libpub = require('libnpm/publish') +const libunpub = require('libnpm/unpublish') const lifecycle = BB.promisify(require('./utils/lifecycle.js')) const log = require('npmlog') -const mapToRegistry = require('./utils/map-to-registry.js') -const npa = require('npm-package-arg') -const npm = require('./npm.js') +const npa = require('libnpm/parse-arg') +const npmConfig = require('./config/figgy-config.js') const output = require('./utils/output.js') +const otplease = require('./utils/otplease.js') const pack = require('./pack') -const pacote = require('pacote') +const { tarball, extract } = require('libnpm') const pacoteOpts = require('./config/pacote') const path = require('path') +const readFileAsync = BB.promisify(require('graceful-fs').readFile) const readJson = BB.promisify(require('read-package-json')) -const readUserInfo = require('./utils/read-user-info.js') const semver = require('semver') const statAsync = BB.promisify(require('graceful-fs').stat) @@ -31,6 +32,16 @@ publish.completion = function (opts, cb) { return cb() } +const PublishConfig = figgyPudding({ + dryRun: 'dry-run', + 'dry-run': { default: false }, + force: { default: false }, + json: { default: false }, + Promise: { default: () => Promise }, + tag: { default: 'latest' }, + tmp: {} +}) + module.exports = publish function publish (args, isRetry, cb) { if (typeof cb !== 'function') { @@ -42,15 +53,16 @@ function publish (args, isRetry, cb) { log.verbose('publish', args) - const t = npm.config.get('tag').trim() + const opts = PublishConfig(npmConfig()) + const t = opts.tag.trim() if (semver.validRange(t)) { return cb(new Error('Tag name must not be a valid SemVer range: ' + t)) } - return publish_(args[0]) + return publish_(args[0], opts) .then((tarball) => { const silent = log.level === 'silent' - if (!silent && npm.config.get('json')) { + if (!silent && opts.json) { output(JSON.stringify(tarball, null, 2)) } else if (!silent) { output(`+ ${tarball.id}`) @@ -59,7 +71,7 @@ function publish (args, isRetry, cb) { .nodeify(cb) } -function publish_ (arg) { +function publish_ (arg, opts) { return statAsync(arg).then((stat) => { if (stat.isDirectory()) { return stat @@ -69,17 +81,17 @@ function publish_ (arg) { throw err } }).then(() => { - return publishFromDirectory(arg) + return publishFromDirectory(arg, opts) }, (err) => { if (err.code !== 'ENOENT' && err.code !== 'ENOTDIR') { throw err } else { - return publishFromPackage(arg) + return publishFromPackage(arg, opts) } }) } -function publishFromDirectory (arg) { +function publishFromDirectory (arg, opts) { // All this readJson is because any of the given scripts might modify the // package.json in question, so we need to refresh after every step. let contents @@ -90,12 +102,12 @@ function publishFromDirectory (arg) { }).then(() => { return readJson(path.join(arg, 'package.json')) }).then((pkg) => { - return cacache.tmp.withTmp(npm.tmp, {tmpPrefix: 'fromDir'}, (tmpDir) => { + return cacache.tmp.withTmp(opts.tmp, {tmpPrefix: 'fromDir'}, (tmpDir) => { const target = path.join(tmpDir, 'package.tgz') return pack.packDirectory(pkg, arg, target, null, true) .tap((c) => { contents = c }) - .then((c) => !npm.config.get('json') && pack.logContents(c)) - .then(() => upload(arg, pkg, false, target)) + .then((c) => !opts.json && pack.logContents(c)) + .then(() => upload(pkg, false, target, opts)) }) }).then(() => { return readJson(path.join(arg, 'package.json')) @@ -107,121 +119,51 @@ function publishFromDirectory (arg) { .then(() => contents) } -function publishFromPackage (arg) { - return cacache.tmp.withTmp(npm.tmp, {tmpPrefix: 'fromPackage'}, (tmp) => { +function publishFromPackage (arg, opts) { + return cacache.tmp.withTmp(opts.tmp, {tmpPrefix: 'fromPackage'}, tmp => { const extracted = path.join(tmp, 'package') const target = path.join(tmp, 'package.json') - const opts = pacoteOpts() - return pacote.tarball.toFile(arg, target, opts) - .then(() => pacote.extract(arg, extracted, opts)) + const pacOpts = pacoteOpts() + return tarball.toFile(arg, target, pacOpts) + .then(() => extract(arg, extracted, pacOpts)) .then(() => readJson(path.join(extracted, 'package.json'))) .then((pkg) => { return BB.resolve(pack.getContents(pkg, target)) - .tap((c) => !npm.config.get('json') && pack.logContents(c)) - .tap(() => upload(arg, pkg, false, target)) + .tap((c) => !opts.json && pack.logContents(c)) + .tap(() => upload(pkg, false, target, opts)) }) }) } -function upload (arg, pkg, isRetry, cached) { - if (!pkg) { - return BB.reject(new Error('no package.json file found')) - } - if (pkg.private) { - return BB.reject(new Error( - 'This package has been marked as private\n' + - "Remove the 'private' field from the package.json to publish it." - )) - } - const mappedConfig = getPublishConfig( - pkg.publishConfig, - npm.config, - npm.registry - ) - const config = mappedConfig.config - const registry = mappedConfig.client - - pkg._npmVersion = npm.version - pkg._nodeVersion = process.versions.node - - delete pkg.modules - - return BB.fromNode((cb) => { - mapToRegistry(pkg.name, config, (err, registryURI, auth, registryBase) => { - if (err) { return cb(err) } - cb(null, [registryURI, auth, registryBase]) - }) - }).spread((registryURI, auth, registryBase) => { - // we just want the base registry URL in this case - log.verbose('publish', 'registryBase', registryBase) - log.silly('publish', 'uploading', cached) - - pkg._npmUser = { - name: auth.username, - email: auth.email - } - - const params = { - metadata: pkg, - body: !npm.config.get('dry-run') && createReadStream(cached), - auth: auth - } - - function closeFile () { - if (!npm.config.get('dry-run')) { - params.body.close() - } - } - - // registry-frontdoor cares about the access level, which is only - // configurable for scoped packages - if (config.get('access')) { - if (!npa(pkg.name).scope && config.get('access') === 'restricted') { - throw new Error("Can't restrict access to unscoped packages.") - } - - params.access = config.get('access') - } - - if (npm.config.get('dry-run')) { - log.verbose('publish', '--dry-run mode enabled. Skipping upload.') - return BB.resolve() - } - - log.showProgress('publish:' + pkg._id) - return BB.fromNode((cb) => { - registry.publish(registryBase, params, cb) - }).catch((err) => { - if ( - err.code === 'EPUBLISHCONFLICT' && - npm.config.get('force') && - !isRetry - ) { - log.warn('publish', 'Forced publish over ' + pkg._id) - return BB.fromNode((cb) => { - npm.commands.unpublish([pkg._id], cb) - }).finally(() => { - // close the file we are trying to upload, we will open it again. - closeFile() - // ignore errors. Use the force. Reach out with your feelings. - return upload(arg, pkg, true, cached).catch(() => { - // but if it fails again, then report the first error. - throw err +function upload (pkg, isRetry, cached, opts) { + if (!opts.dryRun) { + return readFileAsync(cached).then(tarball => { + return otplease(opts, opts => { + return libpub(pkg, tarball, opts) + }).catch(err => { + if ( + err.code === 'EPUBLISHCONFLICT' && + opts.force && + !isRetry + ) { + log.warn('publish', 'Forced publish over ' + pkg._id) + return otplease(opts, opts => libunpub( + npa.resolve(pkg.name, pkg.version), opts + )).finally(() => { + // ignore errors. Use the force. Reach out with your feelings. + return otplease(opts, opts => { + return upload(pkg, true, tarball, opts) + }).catch(() => { + // but if it fails again, then report the first error. + throw err + }) }) - }) - } else { - // close the file we are trying to upload, all attempts to resume will open it again - closeFile() - throw err - } - }) - }).catch((err) => { - if (err.code !== 'EOTP' && !(err.code === 'E401' && /one-time pass/.test(err.message))) throw err - // we prompt on stdout and read answers from stdin, so they need to be ttys. - if (!process.stdin.isTTY || !process.stdout.isTTY) throw err - return readUserInfo.otp().then((otp) => { - npm.config.set('otp', otp) - return upload(arg, pkg, isRetry, cached) + } else { + throw err + } + }) }) - }) + } else { + return opts.Promise.resolve(true) + } } diff --git a/test/tap/publish-config.js b/test/tap/publish-config.js index 0566795dbe3f5..14fd40311a7c0 100644 --- a/test/tap/publish-config.js +++ b/test/tap/publish-config.js @@ -47,9 +47,13 @@ test(function (t) { // itself functions normally. // // Make sure that we don't sit around waiting for lock files - child = common.npm(['publish', '--userconfig=' + pkg + '/fixture_npmrc', '--tag=beta'], { + child = common.npm([ + 'publish', + '--userconfig=' + pkg + '/fixture_npmrc', + '--tag=beta', + '--loglevel', 'error' + ], { cwd: pkg, - stdio: 'inherit', env: { 'npm_config_cache_lock_stale': 1000, 'npm_config_cache_lock_wait': 1000, @@ -58,7 +62,9 @@ test(function (t) { PATH: process.env.PATH, USERPROFILE: osenv.home() } - }, function (err, code) { + }, function (err, code, stdout, stderr) { + t.comment(stdout) + t.comment(stderr) t.ifError(err, 'publish command finished successfully') t.notOk(code, 'npm install exited with code 0') })