Skip to content

Instantly share code, notes, and snippets.

@coderbyheart
Last active August 14, 2026 13:50
Show Gist options
  • Select an option

  • Save coderbyheart/980b245791a834a1e2ef62b22cf79855 to your computer and use it in GitHub Desktop.

Select an option

Save coderbyheart/980b245791a834a1e2ef62b22cf79855 to your computer and use it in GitHub Desktop.
Publish a pre-release ("dev") version to NPM.
/*
* Publish a pre-release ("dev") version to NPM.
*
* Computes the next dev version as `<next-target>-dev.<n>`, where:
* - <next-target> is the latest published stable version with either the
* minor or the major bumped by one and lower components reset to 0
* (e.g. `14.0.4` → `14.1.0` for a minor bump, or `14.0.4` → `15.0.0`
* for a major bump). The bump kind is controlled by the `--major` flag;
* the default is a minor bump.
* - <n> is one greater than the highest existing dev counter for
* <next-target>, or 1 if no dev release exists yet for it.
*
* The version is written to package.json, `npm publish --tag dev` is run,
* and package.json is restored afterwards.
*/
import { execSync } from 'node:child_process'
import { readFileSync, writeFileSync } from 'node:fs'
import path from 'node:path'
const packageJsonPath = path.join(process.cwd(), 'package.json')
const parseStable = (
v: string,
): { major: number; minor: number; patch: number } | null => {
const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v)
if (m === null) return null
return {
major: parseInt(m[1]!, 10),
minor: parseInt(m[2]!, 10),
patch: parseInt(m[3]!, 10),
}
}
const cmp = (
a: { major: number; minor: number; patch: number },
b: { major: number; minor: number; patch: number },
): number => a.major - b.major || a.minor - b.minor || a.patch - b.patch
const packageName = (
JSON.parse(readFileSync(packageJsonPath, 'utf8')) as { name: string }
).name
const versionsOutput = execSync(`npm view ${packageName} versions --json`, {
encoding: 'utf8',
})
const parsedVersions = JSON.parse(versionsOutput) as string | string[]
const versions =
typeof parsedVersions === 'string' ? [parsedVersions] : parsedVersions
const stableVersions = versions
.map(parseStable)
.filter(
(v): v is { major: number; minor: number; patch: number } => v !== null,
)
if (stableVersions.length === 0) {
throw new Error(`No stable versions found for ${packageName}`)
}
const latestStable = stableVersions.reduce((a, b) => (cmp(a, b) >= 0 ? a : b))
const bumpMajor = process.argv.slice(2).includes('--major')
const bumpedTarget = bumpMajor
? { major: latestStable.major + 1, minor: 0, patch: 0 }
: { major: latestStable.major, minor: latestStable.minor + 1, patch: 0 }
// A dev line may already be ahead of what bumping the latest stable would
// produce (e.g. stable is still on 8.x while a 16.x-dev line is underway).
// In that case, keep targeting the existing dev line instead of proposing
// a lower version.
const devTargetPattern = /^(\d+)\.(\d+)\.(\d+)-dev\.\d+$/
const existingDevTargets = versions
.map((v) => devTargetPattern.exec(v))
.filter((m): m is RegExpExecArray => m !== null)
.map((m) => ({
major: parseInt(m[1]!, 10),
minor: parseInt(m[2]!, 10),
patch: parseInt(m[3]!, 10),
}))
const target = existingDevTargets.reduce(
(a, b) => (cmp(a, b) >= 0 ? a : b),
bumpedTarget,
)
const nextTarget = `${target.major}.${target.minor}.${target.patch}`
const devPattern = new RegExp(
`^${nextTarget.replace(/\./g, '\\.')}-dev\\.(\\d+)$`,
)
const devCounters = versions
.map((v) => devPattern.exec(v))
.filter((m): m is RegExpExecArray => m !== null)
.map((m) => parseInt(m[1]!, 10))
const nextDev = (devCounters.length === 0 ? 0 : Math.max(...devCounters)) + 1
const nextVersion = `${nextTarget}-dev.${nextDev}`
console.log(
`Latest stable: ${latestStable.major}.${latestStable.minor}.${latestStable.patch}`,
)
console.log(`Publishing pre-release: ${nextVersion}`)
const originalPackageJson = readFileSync(packageJsonPath, 'utf8')
const pkg = JSON.parse(originalPackageJson) as { version: string }
pkg.version = nextVersion
const trailingNewline = originalPackageJson.endsWith('\n') ? '\n' : ''
writeFileSync(
packageJsonPath,
JSON.stringify(pkg, null, 2) + trailingNewline,
'utf8',
)
try {
execSync(`npm publish --tag dev`, {
stdio: 'inherit',
cwd: process.cwd(),
})
} finally {
writeFileSync(packageJsonPath, originalPackageJson, 'utf8')
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment