Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v9 unpublish bugfixes backport #7050

Merged
merged 2 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions docs/lib/content/commands/npm-unpublish.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@ removing the tarball.
The npm registry will return an error if you are not [logged
in](/commands/npm-adduser).

If you do not specify a version or if you remove all of a package's
versions then the registry will remove the root package entry entirely.
If you do not specify a package name at all, the name and version to be
unpublished will be pulled from the project in the current directory.

If you specify a package name but do not specify a version or if you
remove all of a package's versions then the registry will remove the
root package entry entirely.

Even if you unpublish a package version, that specific name and version
combination can never be reused. In order to publish the package again,
Expand Down
109 changes: 61 additions & 48 deletions lib/commands/unpublish.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const libaccess = require('libnpmaccess')
const libunpub = require('libnpmpublish').unpublish
const npa = require('npm-package-arg')
const npmFetch = require('npm-registry-fetch')
const pacote = require('pacote')
const pkgJson = require('@npmcli/package-json')

const { flatten } = require('@npmcli/config/lib/definitions')
Expand All @@ -23,12 +23,12 @@ class Unpublish extends BaseCommand {
static ignoreImplicitWorkspace = false

static async getKeysOfVersions (name, opts) {
const pkgUri = npa(name).escapedName
const json = await npmFetch.json(`${pkgUri}?write=true`, {
const packument = await pacote.packument(name, {
...opts,
spec: name,
query: { write: true },
})
return Object.keys(json.versions)
return Object.keys(packument.versions)
}

static async completion (args, npm) {
Expand Down Expand Up @@ -59,28 +59,43 @@ class Unpublish extends BaseCommand {
return pkgs
}

const versions = await this.getKeysOfVersions(pkgs[0], opts)
const versions = await Unpublish.getKeysOfVersions(pkgs[0], opts)
if (!versions.length) {
return pkgs
} else {
return versions.map(v => `${pkgs[0]}@${v}`)
}
}

async exec (args) {
async exec (args, { localPrefix } = {}) {
if (args.length > 1) {
throw this.usageError()
}

let spec = args.length && npa(args[0])
// workspace mode
if (!localPrefix) {
localPrefix = this.npm.localPrefix
}

const force = this.npm.config.get('force')
const { silent } = this.npm
const dryRun = this.npm.config.get('dry-run')

let spec
if (args.length) {
spec = npa(args[0])
if (spec.type !== 'version' && spec.rawSpec !== '*') {
throw this.usageError(
'Can only unpublish a single version, or the entire project.\n' +
'Tags and ranges are not supported.'
)
}
}

log.silly('unpublish', 'args[0]', args[0])
log.silly('unpublish', 'spec', spec)

if ((!spec || !spec.rawSpec) && !force) {
if (spec?.rawSpec === '*' && !force) {
throw this.usageError(
'Refusing to delete entire project.\n' +
'Run with --force to do this.'
Expand All @@ -89,69 +104,67 @@ class Unpublish extends BaseCommand {

const opts = { ...this.npm.flatOptions }

let pkgName
let pkgVersion
let manifest
let manifestErr
try {
const { content } = await pkgJson.prepare(this.npm.localPrefix)
const { content } = await pkgJson.prepare(localPrefix)
manifest = content
} catch (err) {
manifestErr = err
}
if (spec) {
// If cwd has a package.json with a name that matches the package being
// unpublished, load up the publishConfig
if (manifest && manifest.name === spec.name && manifest.publishConfig) {
flatten(manifest.publishConfig, opts)
}
const versions = await Unpublish.getKeysOfVersions(spec.name, opts)
if (versions.length === 1 && !force) {
throw this.usageError(LAST_REMAINING_VERSION_ERROR)
}
pkgName = spec.name
pkgVersion = spec.type === 'version' ? `@${spec.rawSpec}` : ''
} else {
if (manifestErr) {
if (manifestErr.code === 'ENOENT' || manifestErr.code === 'ENOTDIR') {
if (err.code === 'ENOENT' || err.code === 'ENOTDIR') {
if (!spec) {
// We needed a local package.json to figure out what package to
// unpublish
throw this.usageError()
} else {
throw manifestErr
}
} else {
// folks should know if ANY local package.json had a parsing error.
// They may be relying on `publishConfig` to be loading and we don't
// want to ignore errors in that case.
throw err
}
}

log.verbose('unpublish', manifest)

let pkgVersion // for cli output
if (spec) {
pkgVersion = spec.type === 'version' ? `@${spec.rawSpec}` : ''
} else {
spec = npa.resolve(manifest.name, manifest.version)
if (manifest.publishConfig) {
flatten(manifest.publishConfig, opts)
log.verbose('unpublish', manifest)
pkgVersion = manifest.version ? `@${manifest.version}` : ''
if (!manifest.version && !force) {
throw this.usageError(
'Refusing to delete entire project.\n' +
'Run with --force to do this.'
)
}
}

pkgName = manifest.name
pkgVersion = manifest.version ? `@${manifest.version}` : ''
// If localPrefix has a package.json with a name that matches the package
// being unpublished, load up the publishConfig
if (manifest?.name === spec.name && manifest.publishConfig) {
flatten(manifest.publishConfig, opts)
}

const versions = await Unpublish.getKeysOfVersions(spec.name, opts)
if (versions.length === 1 && spec.rawSpec === versions[0] && !force) {
throw this.usageError(LAST_REMAINING_VERSION_ERROR)
}
if (versions.length === 1) {
pkgVersion = ''
}

if (!dryRun) {
await otplease(this.npm, opts, o => libunpub(spec, o))
}
if (!silent) {
this.npm.output(`- ${pkgName}${pkgVersion}`)
this.npm.output(`- ${spec.name}${pkgVersion}`)
}
}

async execWorkspaces (args) {
await this.setWorkspaces()

const force = this.npm.config.get('force')
if (!force) {
throw this.usageError(
'Refusing to delete entire project(s).\n' +
'Run with --force to do this.'
)
}

for (const name of this.workspaceNames) {
await this.exec([name])
for (const path of this.workspacePaths) {
await this.exec(args, { localPrefix: path })
}
}
}
Expand Down
Loading
Loading