• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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