Last active
August 14, 2026 13:50
-
-
Save coderbyheart/980b245791a834a1e2ef62b22cf79855 to your computer and use it in GitHub Desktop.
Publish a pre-release ("dev") version to NPM.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * 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