• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1const log = require('../utils/log-shim.js')
2const semver = require('semver')
3const pack = require('libnpmpack')
4const libpub = require('libnpmpublish').publish
5const runScript = require('@npmcli/run-script')
6const pacote = require('pacote')
7const npa = require('npm-package-arg')
8const npmFetch = require('npm-registry-fetch')
9const replaceInfo = require('../utils/replace-info.js')
10
11const otplease = require('../utils/otplease.js')
12const { getContents, logTar } = require('../utils/tar.js')
13
14// for historical reasons, publishConfig in package.json can contain ANY config
15// keys that npm supports in .npmrc files and elsewhere.  We *may* want to
16// revisit this at some point, and have a minimal set that's a SemVer-major
17// change that ought to get a RFC written on it.
18const { flatten } = require('@npmcli/config/lib/definitions')
19const pkgJson = require('@npmcli/package-json')
20
21const BaseCommand = require('../base-command.js')
22class Publish extends BaseCommand {
23  static description = 'Publish a package'
24  static name = 'publish'
25  static params = [
26    'tag',
27    'access',
28    'dry-run',
29    'otp',
30    'workspace',
31    'workspaces',
32    'include-workspace-root',
33    'provenance',
34  ]
35
36  static usage = ['<package-spec>']
37  static workspaces = true
38  static ignoreImplicitWorkspace = false
39
40  async exec (args) {
41    if (args.length === 0) {
42      args = ['.']
43    }
44    if (args.length !== 1) {
45      throw this.usageError()
46    }
47
48    log.verbose('publish', replaceInfo(args))
49
50    const unicode = this.npm.config.get('unicode')
51    const dryRun = this.npm.config.get('dry-run')
52    const json = this.npm.config.get('json')
53    const defaultTag = this.npm.config.get('tag')
54    const ignoreScripts = this.npm.config.get('ignore-scripts')
55    const { silent } = this.npm
56
57    if (semver.validRange(defaultTag)) {
58      throw new Error('Tag name must not be a valid SemVer range: ' + defaultTag.trim())
59    }
60
61    const opts = { ...this.npm.flatOptions, progress: false }
62    log.disableProgress()
63
64    // you can publish name@version, ./foo.tgz, etc.
65    // even though the default is the 'file:.' cwd.
66    const spec = npa(args[0])
67    let manifest = await this.getManifest(spec, opts)
68
69    // only run scripts for directory type publishes
70    if (spec.type === 'directory' && !ignoreScripts) {
71      await runScript({
72        event: 'prepublishOnly',
73        path: spec.fetchSpec,
74        stdio: 'inherit',
75        pkg: manifest,
76        banner: !silent,
77      })
78    }
79
80    // we pass dryRun: true to libnpmpack so it doesn't write the file to disk
81    const tarballData = await pack(spec, {
82      ...opts,
83      foregroundScripts: this.npm.config.isDefault('foreground-scripts')
84        ? true
85        : this.npm.config.get('foreground-scripts'),
86      dryRun: true,
87      prefix: this.npm.localPrefix,
88      workspaces: this.workspacePaths,
89    })
90    const pkgContents = await getContents(manifest, tarballData)
91
92    // The purpose of re-reading the manifest is in case it changed,
93    // so that we send the latest and greatest thing to the registry
94    // note that publishConfig might have changed as well!
95    manifest = await this.getManifest(spec, opts, true)
96
97    // JSON already has the package contents
98    if (!json) {
99      logTar(pkgContents, { unicode })
100    }
101
102    const resolved = npa.resolve(manifest.name, manifest.version)
103    const registry = npmFetch.pickRegistry(resolved, opts)
104    const creds = this.npm.config.getCredentialsByURI(registry)
105    const noCreds = !(creds.token || creds.username || creds.certfile && creds.keyfile)
106    const outputRegistry = replaceInfo(registry)
107
108    if (noCreds) {
109      const msg = `This command requires you to be logged in to ${outputRegistry}`
110      if (dryRun) {
111        log.warn('', `${msg} (dry-run)`)
112      } else {
113        throw Object.assign(new Error(msg), { code: 'ENEEDAUTH' })
114      }
115    }
116
117    const access = opts.access === null ? 'default' : opts.access
118    let msg = `Publishing to ${outputRegistry} with tag ${defaultTag} and ${access} access`
119    if (dryRun) {
120      msg = `${msg} (dry-run)`
121    }
122
123    log.notice('', msg)
124
125    if (!dryRun) {
126      await otplease(this.npm, opts, o => libpub(manifest, tarballData, o))
127    }
128
129    if (spec.type === 'directory' && !ignoreScripts) {
130      await runScript({
131        event: 'publish',
132        path: spec.fetchSpec,
133        stdio: 'inherit',
134        pkg: manifest,
135        banner: !silent,
136      })
137
138      await runScript({
139        event: 'postpublish',
140        path: spec.fetchSpec,
141        stdio: 'inherit',
142        pkg: manifest,
143        banner: !silent,
144      })
145    }
146
147    if (!this.suppressOutput) {
148      if (!silent && json) {
149        this.npm.output(JSON.stringify(pkgContents, null, 2))
150      } else if (!silent) {
151        this.npm.output(`+ ${pkgContents.id}`)
152      }
153    }
154
155    return pkgContents
156  }
157
158  async execWorkspaces (args) {
159    // Suppresses JSON output in publish() so we can handle it here
160    this.suppressOutput = true
161
162    const results = {}
163    const json = this.npm.config.get('json')
164    const { silent } = this.npm
165    await this.setWorkspaces()
166
167    for (const [name, workspace] of this.workspaces.entries()) {
168      let pkgContents
169      try {
170        pkgContents = await this.exec([workspace])
171      } catch (err) {
172        if (err.code === 'EPRIVATE') {
173          log.warn(
174            'publish',
175            `Skipping workspace ${
176              this.npm.chalk.green(name)
177            }, marked as ${
178              this.npm.chalk.bold('private')
179            }`
180          )
181          continue
182        }
183        throw err
184      }
185      // This needs to be in-line w/ the rest of the output that non-JSON
186      // publish generates
187      if (!silent && !json) {
188        this.npm.output(`+ ${pkgContents.id}`)
189      } else {
190        results[name] = pkgContents
191      }
192    }
193
194    if (!silent && json) {
195      this.npm.output(JSON.stringify(results, null, 2))
196    }
197  }
198
199  // if it's a directory, read it from the file system
200  // otherwise, get the full metadata from whatever it is
201  // XXX can't pacote read the manifest from a directory?
202  async getManifest (spec, opts, logWarnings = false) {
203    let manifest
204    if (spec.type === 'directory') {
205      const changes = []
206      const pkg = await pkgJson.fix(spec.fetchSpec, { changes })
207      if (changes.length && logWarnings) {
208        /* eslint-disable-next-line max-len */
209        log.warn('publish', 'npm auto-corrected some errors in your package.json when publishing.  Please run "npm pkg fix" to address these errors.')
210        log.warn('publish', `errors corrected:\n${changes.join('\n')}`)
211      }
212      // Prepare is the special function for publishing, different than normalize
213      const { content } = await pkg.prepare()
214      manifest = content
215    } else {
216      manifest = await pacote.manifest(spec, {
217        ...opts,
218        fullmetadata: true,
219        fullReadJson: true,
220      })
221    }
222    if (manifest.publishConfig) {
223      flatten(manifest.publishConfig, opts)
224    }
225    return manifest
226  }
227}
228module.exports = Publish
229