1 | 'use strict'
|
2 |
|
3 | const BB = require('bluebird')
|
4 |
|
5 | const cacache = require('cacache')
|
6 | const figgyPudding = require('figgy-pudding')
|
7 | const libpub = require('libnpm/publish')
|
8 | const libunpub = require('libnpm/unpublish')
|
9 | const lifecycle = BB.promisify(require('./utils/lifecycle.js'))
|
10 | const log = require('npmlog')
|
11 | const npa = require('libnpm/parse-arg')
|
12 | const npmConfig = require('./config/figgy-config.js')
|
13 | const output = require('./utils/output.js')
|
14 | const otplease = require('./utils/otplease.js')
|
15 | const pack = require('./pack')
|
16 | const { tarball, extract } = require('libnpm')
|
17 | const path = require('path')
|
18 | const readFileAsync = BB.promisify(require('graceful-fs').readFile)
|
19 | const readJson = BB.promisify(require('read-package-json'))
|
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 | const 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 |
|
44 | module.exports = publish
|
45 | function 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 |
|
73 | function 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 |
|
93 | function publishFromDirectory (arg, opts) {
|
94 |
|
95 |
|
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 |
|
121 | function 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 |
|
136 | function 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 |
|
152 | return otplease(opts, opts => {
|
153 | return upload(pkg, true, tarball, opts)
|
154 | }).catch(() => {
|
155 |
|
156 | throw err
|
157 | })
|
158 | })
|
159 | } else {
|
160 | throw err
|
161 | }
|
162 | })
|
163 | })
|
164 | } else {
|
165 | return opts.Promise.resolve(true)
|
166 | }
|
167 | }
|