Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
publish.js278 linesDownload Raw Back to commands
1const { log, output } = require('proc-log')
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 { redactLog: replaceInfo } = require('@npmcli/redact')
10const { otplease } = require('../utils/auth.js')
11const { getContents, logTar } = require('../utils/tar.js')
12// for historical reasons, publishConfig in package.json can contain ANY config keys that npm supports in .npmrc files and elsewhere.
13// We *may* want to revisit this at some point, and have a minimal set that's a SemVer-major change that ought to get a RFC written on it.
14const { flatten } = require('@npmcli/config/lib/definitions')
15const pkgJson = require('@npmcli/package-json')
16const BaseCommand = require('../base-cmd.js')
17const { oidc } = require('../../lib/utils/oidc.js')
18
19class Publish extends BaseCommand {
20  static description = 'Publish a package'
21  static name = 'publish'
22  static params = [
23    'tag',
24    'access',
25    'dry-run',
26    'otp',
27    'workspace',
28    'workspaces',
29    'include-workspace-root',
30    'provenance',
31  ]
32
33  static usage = ['<package-spec>']
34  static workspaces = true
35  static ignoreImplicitWorkspace = false
36
37  async exec (args) {
38    if (args.length === 0) {
39      args = ['.']
40    }
41    if (args.length !== 1) {
42      throw this.usageError()
43    }
44
45    await this.#publish(args)
46  }
47
48  async execWorkspaces (args) {
49    const useWorkspaces = args.length === 0 || args.includes('.')
50    if (!useWorkspaces) {
51      log.warn('Ignoring workspaces for specified package(s)')
52      return this.exec(args)
53    }
54    await this.setWorkspaces()
55
56    for (const [name, workspace] of this.workspaces.entries()) {
57      try {
58        await this.#publish([workspace], { workspace: name })
59      } catch (err) {
60        if (err.code !== 'EPRIVATE') {
61          throw err
62        }
63        log.warn('publish', `Skipping workspace ${this.npm.chalk.cyan(name)}, marked as ${this.npm.chalk.bold('private')}`)
64      }
65    }
66  }
67
68  async #publish (args, { workspace } = {}) {
69    log.verbose('publish', replaceInfo(args))
70
71    const unicode = this.npm.config.get('unicode')
72    const dryRun = this.npm.config.get('dry-run')
73    const json = this.npm.config.get('json')
74    const defaultTag = this.npm.config.get('tag')
75    const ignoreScripts = this.npm.config.get('ignore-scripts')
76    const { silent } = this.npm
77
78    if (semver.validRange(defaultTag)) {
79      throw new Error('Tag name must not be a valid SemVer range: ' + defaultTag.trim())
80    }
81
82    const opts = { ...this.npm.flatOptions, progress: false }
83
84    // you can publish name@version, ./foo.tgz, etc even though the default is the 'file:.' cwd.
85    const spec = npa(args[0])
86    let manifest = await this.#getManifest(spec, opts)
87
88    // only run scripts for directory type publishes
89    if (spec.type === 'directory' && !ignoreScripts) {
90      await runScript({
91        event: 'prepublishOnly',
92        path: spec.fetchSpec,
93        stdio: 'inherit',
94        pkg: manifest,
95      })
96    }
97
98    // we pass dryRun: true to libnpmpack so it doesn't write the file to disk
99    const tarballData = await pack(spec, {
100      ...opts,
101      foregroundScripts: this.npm.config.isDefault('foreground-scripts')
102        ? true
103        : this.npm.config.get('foreground-scripts'),
104      dryRun: true,
105      prefix: this.npm.localPrefix,
106      workspaces: this.workspacePaths,
107    })
108    const pkgContents = await getContents(manifest, tarballData)
109    const logPkg = () => logTar(pkgContents, { unicode, json, key: workspace })
110
111    // The purpose of re-reading the manifest is in case it changed, so that we send the latest and greatest thing to the registry note that publishConfig might have changed as well!
112    manifest = await this.#getManifest(spec, opts, true)
113    const force = this.npm.config.get('force')
114    const isDefaultTag = this.npm.config.isDefault('tag') && !manifest.publishConfig?.tag
115
116    if (!force) {
117      const isPreRelease = Boolean(semver.parse(manifest.version).prerelease.length)
118      if (isPreRelease && isDefaultTag) {
119        throw new Error('You must specify a tag using --tag when publishing a prerelease version.')
120      }
121    }
122
123    // If we are not in JSON mode then we show the user the contents of the tarball before it is published so they can see it while their otp is pending
124    if (!json) {
125      logPkg()
126    }
127
128    const resolved = npa.resolve(manifest.name, manifest.version)
129
130    // make sure tag is valid, this will throw if invalid
131    npa(`${manifest.name}@${defaultTag}`)
132
133    const registry = npmFetch.pickRegistry(resolved, opts)
134
135    await oidc({ packageName: manifest.name, registry, opts, config: this.npm.config })
136
137    const creds = this.npm.config.getCredentialsByURI(registry)
138    const noCreds = !(creds.token || creds.username || creds.certfile && creds.keyfile)
139    const outputRegistry = replaceInfo(registry)
140
141    // if a workspace package is marked private then we skip it
142    if (workspace && manifest.private) {
143      throw Object.assign(
144        new Error(`This package has been marked as private
145  Remove the 'private' field from the package.json to publish it.`),
146        { code: 'EPRIVATE' }
147      )
148    }
149
150    if (noCreds) {
151      const msg = `This command requires you to be logged in to ${outputRegistry}`
152      if (dryRun) {
153        log.warn('', `${msg} (dry-run)`)
154      } else {
155        throw Object.assign(new Error(msg), { code: 'ENEEDAUTH' })
156      }
157    }
158
159    if (!force) {
160      const { highestVersion, versions } = await this.#registryVersions(resolved, registry)
161      /* eslint-disable-next-line max-len */
162      const highestVersionIsGreater = !!highestVersion && semver.gte(highestVersion, manifest.version)
163
164      if (versions.includes(manifest.version)) {
165        throw new Error(`You cannot publish over the previously published versions: ${manifest.version}.`)
166      }
167
168      if (highestVersionIsGreater && isDefaultTag) {
169        throw new Error(`Cannot implicitly apply the "latest" tag because previously published version ${highestVersion} is higher than the new version ${manifest.version}. You must specify a tag using --tag.`)
170      }
171    }
172
173    const access = opts.access === null ? 'default' : opts.access
174    let msg = `Publishing to ${outputRegistry} with tag ${defaultTag} and ${access} access`
175    if (dryRun) {
176      msg = `${msg} (dry-run)`
177    }
178
179    log.notice('', msg)
180
181    if (!dryRun) {
182      await otplease(this.npm, opts, o => libpub(manifest, tarballData, o))
183    }
184
185    // In json mode we don't log until the publish has completed as this will add it to the output only if completes successfully
186    if (json) {
187      logPkg()
188    }
189
190    if (spec.type === 'directory' && !ignoreScripts) {
191      await runScript({
192        event: 'publish',
193        path: spec.fetchSpec,
194        stdio: 'inherit',
195        pkg: manifest,
196      })
197
198      await runScript({
199        event: 'postpublish',
200        path: spec.fetchSpec,
201        stdio: 'inherit',
202        pkg: manifest,
203      })
204    }
205
206    if (!json && !silent) {
207      output.standard(`+ ${pkgContents.id}`)
208    }
209  }
210
211  async #registryVersions (spec, registry) {
212    try {
213      const packument = await pacote.packument(spec, {
214        ...this.npm.flatOptions,
215        preferOnline: true,
216        registry,
217        _isRoot: true,
218      })
219      if (typeof packument?.versions === 'undefined') {
220        return { versions: [], highestVersion: null }
221      }
222      const ordered = Object.keys(packument?.versions)
223        .flatMap(v => {
224          const s = new semver.SemVer(v)
225          if ((s.prerelease.length > 0) || packument.versions[v].deprecated) {
226            return []
227          }
228          return s
229        })
230        .sort((a, b) => b.compare(a))
231      const highestVersion = ordered.length >= 1 ? ordered[0].version : null
232      const versions = ordered.map(v => v.version)
233      return { versions, highestVersion }
234    } catch (e) {
235      return { versions: [], highestVersion: null }
236    }
237  }
238
239  // if it's a directory, read it from the file system
240  // otherwise, get the full metadata from whatever it is
241  // XXX can't pacote read the manifest from a directory?
242  async #getManifest (spec, opts, logWarnings = false) {
243    let manifest
244    if (spec.type === 'directory') {
245      const changes = []
246      const pkg = await pkgJson.fix(spec.fetchSpec, { changes })
247      if (changes.length && logWarnings) {
248        log.warn('publish', 'npm auto-corrected some errors in your package.json when publishing.  Please run "npm pkg fix" to address these errors.')
249        log.warn('publish', `errors corrected:\n${changes.join('\n')}`)
250      }
251      // Prepare is the special function for publishing, different than normalize
252      const { content } = await pkg.prepare()
253      manifest = content
254    } else {
255      manifest = await pacote.manifest(spec, {
256        ...opts,
257        fullmetadata: true,
258        fullReadJson: true,
259      })
260    }
261    if (manifest.publishConfig) {
262      const cliFlags = this.npm.config.data.get('cli').raw
263      // Filter out properties set in CLI flags to prioritize them over corresponding `publishConfig` settings
264      const filteredPublishConfig = Object.fromEntries(
265        Object.entries(manifest.publishConfig).filter(([key]) => !(key in cliFlags)))
266      if (logWarnings) {
267        for (const key in filteredPublishConfig) {
268          this.npm.config.checkUnknown('publishConfig', key)
269        }
270      }
271      flatten(filteredPublishConfig, opts)
272    }
273    return manifest
274  }
275}
276
277module.exports = Publish
278 
codekingpro/portable-devtools · Team Ai