1'use strict' 2 3const BB = require('bluebird') 4 5const cacache = require('cacache') 6const figgyPudding = require('figgy-pudding') 7const libpub = require('libnpm/publish') 8const libunpub = require('libnpm/unpublish') 9const lifecycle = BB.promisify(require('./utils/lifecycle.js')) 10const log = require('npmlog') 11const npa = require('libnpm/parse-arg') 12const npmConfig = require('./config/figgy-config.js') 13const output = require('./utils/output.js') 14const otplease = require('./utils/otplease.js') 15const pack = require('./pack') 16const { tarball, extract } = require('libnpm') 17const path = require('path') 18const readFileAsync = BB.promisify(require('graceful-fs').readFile) 19const readJson = BB.promisify(require('read-package-json')) 20const semver = require('semver') 21const statAsync = BB.promisify(require('graceful-fs').stat) 22 23publish.usage = 'npm publish [<tarball>|<folder>] [--tag <tag>] [--access <public|restricted>] [--dry-run]' + 24 "\n\nPublishes '.' if no argument supplied" + 25 '\n\nSets tag `latest` if no --tag specified' 26 27publish.completion = function (opts, cb) { 28 // publish can complete to a folder with a package.json 29 // or a tarball, or a tarball url. 30 // for now, not yet implemented. 31 return cb() 32} 33 34const PublishConfig = figgyPudding({ 35 dryRun: 'dry-run', 36 'dry-run': { default: false }, 37 force: { default: false }, 38 json: { default: false }, 39 Promise: { default: () => Promise }, 40 tag: { default: 'latest' }, 41 tmp: {} 42}) 43 44module.exports = publish 45function publish (args, isRetry, cb) { 46 if (typeof cb !== 'function') { 47 cb = isRetry 48 isRetry = false 49 } 50 if (args.length === 0) args = ['.'] 51 if (args.length !== 1) return cb(publish.usage) 52 53 log.verbose('publish', args) 54 55 const opts = PublishConfig(npmConfig()) 56 const t = opts.tag.trim() 57 if (semver.validRange(t)) { 58 return cb(new Error('Tag name must not be a valid SemVer range: ' + t)) 59 } 60 61 return publish_(args[0], opts) 62 .then((tarball) => { 63 const silent = log.level === 'silent' 64 if (!silent && opts.json) { 65 output(JSON.stringify(tarball, null, 2)) 66 } else if (!silent) { 67 output(`+ ${tarball.id}`) 68 } 69 }) 70 .nodeify(cb) 71} 72 73function publish_ (arg, opts) { 74 return statAsync(arg).then((stat) => { 75 if (stat.isDirectory()) { 76 return stat 77 } else { 78 const err = new Error('not a directory') 79 err.code = 'ENOTDIR' 80 throw err 81 } 82 }).then(() => { 83 return publishFromDirectory(arg, opts) 84 }, (err) => { 85 if (err.code !== 'ENOENT' && err.code !== 'ENOTDIR') { 86 throw err 87 } else { 88 return publishFromPackage(arg, opts) 89 } 90 }) 91} 92 93function publishFromDirectory (arg, opts) { 94 // All this readJson is because any of the given scripts might modify the 95 // package.json in question, so we need to refresh after every step. 96 let contents 97 return pack.prepareDirectory(arg).then(() => { 98 return readJson(path.join(arg, 'package.json')) 99 }).then((pkg) => { 100 return lifecycle(pkg, 'prepublishOnly', arg) 101 }).then(() => { 102 return readJson(path.join(arg, 'package.json')) 103 }).then((pkg) => { 104 return cacache.tmp.withTmp(opts.tmp, {tmpPrefix: 'fromDir'}, (tmpDir) => { 105 const target = path.join(tmpDir, 'package.tgz') 106 return pack.packDirectory(pkg, arg, target, null, true) 107 .tap((c) => { contents = c }) 108 .then((c) => !opts.json && pack.logContents(c)) 109 .then(() => upload(pkg, false, target, opts)) 110 }) 111 }).then(() => { 112 return readJson(path.join(arg, 'package.json')) 113 }).tap((pkg) => { 114 return lifecycle(pkg, 'publish', arg) 115 }).tap((pkg) => { 116 return lifecycle(pkg, 'postpublish', arg) 117 }) 118 .then(() => contents) 119} 120 121function publishFromPackage (arg, opts) { 122 return cacache.tmp.withTmp(opts.tmp, {tmpPrefix: 'fromPackage'}, tmp => { 123 const extracted = path.join(tmp, 'package') 124 const target = path.join(tmp, 'package.json') 125 return tarball.toFile(arg, target, opts) 126 .then(() => extract(arg, extracted, opts)) 127 .then(() => readJson(path.join(extracted, 'package.json'))) 128 .then((pkg) => { 129 return BB.resolve(pack.getContents(pkg, target)) 130 .tap((c) => !opts.json && pack.logContents(c)) 131 .tap(() => upload(pkg, false, target, opts)) 132 }) 133 }) 134} 135 136function upload (pkg, isRetry, cached, opts) { 137 if (!opts.dryRun) { 138 return readFileAsync(cached).then(tarball => { 139 return otplease(opts, opts => { 140 return libpub(pkg, tarball, opts) 141 }).catch(err => { 142 if ( 143 err.code === 'EPUBLISHCONFLICT' && 144 opts.force && 145 !isRetry 146 ) { 147 log.warn('publish', 'Forced publish over ' + pkg._id) 148 return otplease(opts, opts => libunpub( 149 npa.resolve(pkg.name, pkg.version), opts 150 )).finally(() => { 151 // ignore errors. Use the force. Reach out with your feelings. 152 return otplease(opts, opts => { 153 return upload(pkg, true, tarball, opts) 154 }).catch(() => { 155 // but if it fails again, then report the first error. 156 throw err 157 }) 158 }) 159 } else { 160 throw err 161 } 162 }) 163 }) 164 } else { 165 return opts.Promise.resolve(true) 166 } 167} 168