Skip to content

Commit

Permalink
fix(publish): honor force for no dist tag and registry version check (n…
Browse files Browse the repository at this point in the history
…pm#8054)

Merges npm#7993 /
npm#7994 /
npm#7995

- [x] adds ability to --force publish without latest check
- [x] adds ability to --force publish of prerelease without tag
- [x] consider equality in publish dist tag check error message
  • Loading branch information
reggi authored Jan 22, 2025
1 parent 7ddfbad commit 31455b2
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 22 deletions.
35 changes: 23 additions & 12 deletions lib/commands/publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -202,15 +211,15 @@ class Publish extends BaseCommand {
}
}

async #highestPublishedVersion (spec, registry) {
async #registryVersions (spec, registry) {
try {
const packument = await pacote.packument(spec, {
...this.npm.flatOptions,
preferOnline: true,
registry,
})
if (typeof packument?.versions === 'undefined') {
return null
return { versions: [], highestVersion: null }
}
const ordered = Object.keys(packument?.versions)
.flatMap(v => {
Expand All @@ -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 }
}
}

Expand Down
20 changes: 11 additions & 9 deletions mock-registry/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down
52 changes: 51 additions & 1 deletion test/lib/commands/publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})

Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -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', [])
})
})

0 comments on commit 31455b2

Please sign in to comment.