UNPKG

6.93 kBJavaScriptView Raw
1'use strict'
2
3const BB = require('bluebird')
4
5const cacache = require('cacache')
6const createReadStream = require('graceful-fs').createReadStream
7const getPublishConfig = require('./utils/get-publish-config.js')
8const lifecycle = BB.promisify(require('./utils/lifecycle.js'))
9const log = require('npmlog')
10const mapToRegistry = require('./utils/map-to-registry.js')
11const npa = require('npm-package-arg')
12const npm = require('./npm.js')
13const output = require('./utils/output.js')
14const pack = require('./pack')
15const pacote = require('pacote')
16const pacoteOpts = require('./config/pacote')
17const path = require('path')
18const readJson = BB.promisify(require('read-package-json'))
19const readUserInfo = require('./utils/read-user-info.js')
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
34module.exports = publish
35function publish (args, isRetry, cb) {
36 if (typeof cb !== 'function') {
37 cb = isRetry
38 isRetry = false
39 }
40 if (args.length === 0) args = ['.']
41 if (args.length !== 1) return cb(publish.usage)
42
43 log.verbose('publish', args)
44
45 const t = npm.config.get('tag').trim()
46 if (semver.validRange(t)) {
47 return cb(new Error('Tag name must not be a valid SemVer range: ' + t))
48 }
49
50 return publish_(args[0])
51 .then((tarball) => {
52 const silent = log.level === 'silent'
53 if (!silent && npm.config.get('json')) {
54 output(JSON.stringify(tarball, null, 2))
55 } else if (!silent) {
56 output(`+ ${tarball.id}`)
57 }
58 })
59 .nodeify(cb)
60}
61
62function publish_ (arg) {
63 return statAsync(arg).then((stat) => {
64 if (stat.isDirectory()) {
65 return stat
66 } else {
67 const err = new Error('not a directory')
68 err.code = 'ENOTDIR'
69 throw err
70 }
71 }).then(() => {
72 return publishFromDirectory(arg)
73 }, (err) => {
74 if (err.code !== 'ENOENT' && err.code !== 'ENOTDIR') {
75 throw err
76 } else {
77 return publishFromPackage(arg)
78 }
79 })
80}
81
82function publishFromDirectory (arg) {
83 // All this readJson is because any of the given scripts might modify the
84 // package.json in question, so we need to refresh after every step.
85 let contents
86 return pack.prepareDirectory(arg).then(() => {
87 return readJson(path.join(arg, 'package.json'))
88 }).then((pkg) => {
89 return lifecycle(pkg, 'prepublishOnly', arg)
90 }).then(() => {
91 return readJson(path.join(arg, 'package.json'))
92 }).then((pkg) => {
93 return cacache.tmp.withTmp(npm.tmp, {tmpPrefix: 'fromDir'}, (tmpDir) => {
94 const target = path.join(tmpDir, 'package.tgz')
95 return pack.packDirectory(pkg, arg, target, null, true)
96 .tap((c) => { contents = c })
97 .then((c) => !npm.config.get('json') && pack.logContents(c))
98 .then(() => upload(arg, pkg, false, target))
99 })
100 }).then(() => {
101 return readJson(path.join(arg, 'package.json'))
102 }).tap((pkg) => {
103 return lifecycle(pkg, 'publish', arg)
104 }).tap((pkg) => {
105 return lifecycle(pkg, 'postpublish', arg)
106 })
107 .then(() => contents)
108}
109
110function publishFromPackage (arg) {
111 return cacache.tmp.withTmp(npm.tmp, {tmpPrefix: 'fromPackage'}, (tmp) => {
112 const extracted = path.join(tmp, 'package')
113 const target = path.join(tmp, 'package.json')
114 const opts = pacoteOpts()
115 return pacote.tarball.toFile(arg, target, opts)
116 .then(() => pacote.extract(arg, extracted, opts))
117 .then(() => readJson(path.join(extracted, 'package.json')))
118 .then((pkg) => {
119 return BB.resolve(pack.getContents(pkg, target))
120 .tap((c) => !npm.config.get('json') && pack.logContents(c))
121 .tap(() => upload(arg, pkg, false, target))
122 })
123 })
124}
125
126function upload (arg, pkg, isRetry, cached) {
127 if (!pkg) {
128 return BB.reject(new Error('no package.json file found'))
129 }
130 if (pkg.private) {
131 return BB.reject(new Error(
132 'This package has been marked as private\n' +
133 "Remove the 'private' field from the package.json to publish it."
134 ))
135 }
136 const mappedConfig = getPublishConfig(
137 pkg.publishConfig,
138 npm.config,
139 npm.registry
140 )
141 const config = mappedConfig.config
142 const registry = mappedConfig.client
143
144 pkg._npmVersion = npm.version
145 pkg._nodeVersion = process.versions.node
146
147 delete pkg.modules
148
149 return BB.fromNode((cb) => {
150 mapToRegistry(pkg.name, config, (err, registryURI, auth, registryBase) => {
151 if (err) { return cb(err) }
152 cb(null, [registryURI, auth, registryBase])
153 })
154 }).spread((registryURI, auth, registryBase) => {
155 // we just want the base registry URL in this case
156 log.verbose('publish', 'registryBase', registryBase)
157 log.silly('publish', 'uploading', cached)
158
159 pkg._npmUser = {
160 name: auth.username,
161 email: auth.email
162 }
163
164 const params = {
165 metadata: pkg,
166 body: !npm.config.get('dry-run') && createReadStream(cached),
167 auth: auth
168 }
169
170 // registry-frontdoor cares about the access level, which is only
171 // configurable for scoped packages
172 if (config.get('access')) {
173 if (!npa(pkg.name).scope && config.get('access') === 'restricted') {
174 throw new Error("Can't restrict access to unscoped packages.")
175 }
176
177 params.access = config.get('access')
178 }
179
180 if (npm.config.get('dry-run')) {
181 log.verbose('publish', '--dry-run mode enabled. Skipping upload.')
182 return BB.resolve()
183 }
184
185 log.showProgress('publish:' + pkg._id)
186 return BB.fromNode((cb) => {
187 registry.publish(registryBase, params, cb)
188 }).catch((err) => {
189 if (
190 err.code === 'EPUBLISHCONFLICT' &&
191 npm.config.get('force') &&
192 !isRetry
193 ) {
194 log.warn('publish', 'Forced publish over ' + pkg._id)
195 return BB.fromNode((cb) => {
196 npm.commands.unpublish([pkg._id], cb)
197 }).finally(() => {
198 // ignore errors. Use the force. Reach out with your feelings.
199 return upload(arg, pkg, true, cached).catch(() => {
200 // but if it fails again, then report the first error.
201 throw err
202 })
203 })
204 } else {
205 throw err
206 }
207 })
208 }).catch((err) => {
209 if (err.code !== 'EOTP' && !(err.code === 'E401' && /one-time pass/.test(err.message))) throw err
210 // we prompt on stdout and read answers from stdin, so they need to be ttys.
211 if (!process.stdin.isTTY || !process.stdout.isTTY) throw err
212 return readUserInfo.otp().then((otp) => {
213 npm.config.set('otp', otp)
214 return upload(arg, pkg, isRetry, cached)
215 })
216 })
217}