1 | 'use strict'
|
2 |
|
3 | const BB = require('bluebird')
|
4 |
|
5 | const cacache = require('cacache')
|
6 | const createReadStream = require('graceful-fs').createReadStream
|
7 | const getPublishConfig = require('./utils/get-publish-config.js')
|
8 | const lifecycle = BB.promisify(require('./utils/lifecycle.js'))
|
9 | const log = require('npmlog')
|
10 | const mapToRegistry = require('./utils/map-to-registry.js')
|
11 | const npa = require('npm-package-arg')
|
12 | const npm = require('./npm.js')
|
13 | const output = require('./utils/output.js')
|
14 | const pack = require('./pack')
|
15 | const pacote = require('pacote')
|
16 | const pacoteOpts = require('./config/pacote')
|
17 | const path = require('path')
|
18 | const readJson = BB.promisify(require('read-package-json'))
|
19 | const readUserInfo = require('./utils/read-user-info.js')
|
20 | const semver = require('semver')
|
21 | const statAsync = BB.promisify(require('graceful-fs').stat)
|
22 |
|
23 | publish.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 |
|
27 | publish.completion = function (opts, cb) {
|
28 |
|
29 |
|
30 |
|
31 | return cb()
|
32 | }
|
33 |
|
34 | module.exports = publish
|
35 | function 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 |
|
62 | function 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 |
|
82 | function publishFromDirectory (arg) {
|
83 |
|
84 |
|
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 |
|
110 | function 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 |
|
126 | function 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 |
|
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 |
|
171 |
|
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 |
|
199 | return upload(arg, pkg, true, cached).catch(() => {
|
200 |
|
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 |
|
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 | }
|