diff --git a/lib/commands/publish.js b/lib/commands/publish.js index 764a3c15c9999..1967e05a23534 100644 --- a/lib/commands/publish.js +++ b/lib/commands/publish.js @@ -114,12 +114,14 @@ class Publish extends BaseCommand { // so that we send the latest and greatest thing to the registry // note that publishConfig might have changed as well! manifest = await this.#getManifest(spec, opts, true) - - const isPreRelease = Boolean(semver.parse(manifest.version).prerelease.length) + const force = this.npm.config.get('force') const isDefaultTag = this.npm.config.isDefault('tag') && !manifest.publishConfig?.tag - if (isPreRelease && isDefaultTag) { - throw new Error('You must specify a tag using --tag when publishing a prerelease version.') + if (!force) { + const isPreRelease = Boolean(semver.parse(manifest.version).prerelease.length) + if (isPreRelease && isDefaultTag) { + throw new Error('You must specify a tag using --tag when publishing a prerelease version.') + } } // If we are not in JSON mode then we show the user the contents of the tarball @@ -156,11 +158,18 @@ class Publish extends BaseCommand { } } - const latestVersion = await this.#highestPublishedVersion(resolved, registry) - const latestSemverIsGreater = !!latestVersion && semver.gte(latestVersion, manifest.version) + if (!force) { + const { highestVersion, versions } = await this.#registryVersions(resolved, registry) + /* eslint-disable-next-line max-len */ + const highestVersionIsGreater = !!highestVersion && semver.gte(highestVersion, manifest.version) - if (latestSemverIsGreater && isDefaultTag) { - throw new Error(`Cannot implicitly apply the "latest" tag because published version ${latestVersion} is higher than the new version ${manifest.version}. You must specify a tag using --tag.`) + if (versions.includes(manifest.version)) { + throw new Error(`You cannot publish over the previously published versions: ${manifest.version}.`) + } + + if (highestVersionIsGreater && isDefaultTag) { + 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.`) + } } const access = opts.access === null ? 'default' : opts.access @@ -202,7 +211,7 @@ class Publish extends BaseCommand { } } - async #highestPublishedVersion (spec, registry) { + async #registryVersions (spec, registry) { try { const packument = await pacote.packument(spec, { ...this.npm.flatOptions, @@ -210,7 +219,7 @@ class Publish extends BaseCommand { registry, }) if (typeof packument?.versions === 'undefined') { - return null + return { versions: [], highestVersion: null } } const ordered = Object.keys(packument?.versions) .flatMap(v => { @@ -221,9 +230,11 @@ class Publish extends BaseCommand { return s }) .sort((a, b) => b.compare(a)) - return ordered.length >= 1 ? ordered[0].version : null + const highestVersion = ordered.length >= 1 ? ordered[0].version : null + const versions = ordered.map(v => v.version) + return { versions, highestVersion } } catch (e) { - return null + return { versions: [], highestVersion: null } } } diff --git a/mock-registry/lib/index.js b/mock-registry/lib/index.js index 3b06681b7ed31..8248631519054 100644 --- a/mock-registry/lib/index.js +++ b/mock-registry/lib/index.js @@ -359,16 +359,18 @@ class MockRegistry { } publish (name, { - packageJson, access, noPut, putCode, manifest, packuments, + packageJson, access, noGet, noPut, putCode, manifest, packuments, } = {}) { - // this getPackage call is used to get the latest semver version before publish - if (manifest) { - this.getPackage(name, { code: 200, resp: manifest }) - } else if (packuments) { - this.getPackage(name, { code: 200, resp: this.manifest({ name, packuments }) }) - } else { - // assumes the package does not exist yet and will 404 x2 from pacote.manifest - this.getPackage(name, { times: 2, code: 404 }) + if (!noGet) { + // this getPackage call is used to get the latest semver version before publish + if (manifest) { + this.getPackage(name, { code: 200, resp: manifest }) + } else if (packuments) { + this.getPackage(name, { code: 200, resp: this.manifest({ name, packuments }) }) + } else { + // assumes the package does not exist yet and will 404 x2 from pacote.manifest + this.getPackage(name, { times: 2, code: 404 }) + } } if (!noPut) { this.putPackage(name, { code: putCode, packageJson, access }) diff --git a/test/lib/commands/publish.js b/test/lib/commands/publish.js index cd5c1bdad6aa3..f310587418b1f 100644 --- a/test/lib/commands/publish.js +++ b/test/lib/commands/publish.js @@ -853,6 +853,28 @@ t.test('prerelease dist tag', (t) => { await npm.exec('publish', []) }) + t.test('does not abort when prerelease and force', async t => { + const packageJson = { + ...pkgJson, + version: '1.0.0-0', + publishConfig: { registry: alternateRegistry }, + } + const { npm, registry } = await loadNpmWithRegistry(t, { + config: { + loglevel: 'silent', + force: true, + [`${alternateRegistry.slice(6)}/:_authToken`]: 'test-other-token', + }, + prefixDir: { + 'package.json': JSON.stringify(packageJson, null, 2), + }, + registry: alternateRegistry, + authorization: 'test-other-token', + }) + registry.publish(pkg, { noGet: true, packageJson }) + await npm.exec('publish', []) + }) + t.end() }) @@ -886,7 +908,7 @@ t.test('semver highest dist tag', async t => { registry.publish(pkg, { noPut: true, packuments }) await t.rejects(async () => { await npm.exec('publish', []) - }, new Error('Cannot implicitly apply the "latest" tag because published version 100.0.0 is higher than the new version 99.0.0. You must specify a tag using --tag.')) + }, new Error('Cannot implicitly apply the "latest" tag because previously published version 100.0.0 is higher than the new version 99.0.0. You must specify a tag using --tag.')) }) await t.test('ALLOWS publish when highest is HIGHER than publishing version and flag', async t => { @@ -933,4 +955,32 @@ t.test('semver highest dist tag', async t => { registry.publish(pkg, { packuments }) await npm.exec('publish', []) }) + + await t.test('PREVENTS publish when latest version is SAME AS publishing version', async t => { + const version = '100.0.0' + const { npm, registry } = await loadNpmWithRegistry(t, init({ version })) + registry.publish(pkg, { noPut: true, packuments }) + await t.rejects(async () => { + await npm.exec('publish', []) + }, new Error('You cannot publish over the previously published versions: 100.0.0.')) + }) + + await t.test('PREVENTS publish when publishing version EXISTS ALREADY in the registry', async t => { + const version = '50.0.0' + const { npm, registry } = await loadNpmWithRegistry(t, init({ version })) + registry.publish(pkg, { noPut: true, packuments }) + await t.rejects(async () => { + await npm.exec('publish', []) + }, new Error('You cannot publish over the previously published versions: 50.0.0.')) + }) + + await t.test('ALLOWS publish when latest is HIGHER than publishing version and flag --force', async t => { + const version = '99.0.0' + const { npm, registry } = await loadNpmWithRegistry(t, { + ...init({ version }), + argv: ['--force'], + }) + registry.publish(pkg, { noGet: true, packuments }) + await npm.exec('publish', []) + }) })