Last active
June 26, 2021 14:52
-
-
Save mmazzarolo/64c7b0c48085f3ce05ebb2fc3d6c62f7 to your computer and use it in GitHub Desktop.
Bump the version number of an NPM dependency and commits the changes in a new branch
This file contains 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
// Just a Deno script that bumps an NPM dependency of the project | |
// in the current directory. | |
// Might be helpful if you need to automate the bumping of a | |
// dependency across multiple repos. | |
// It: | |
// - Makes sure you start from a clean state | |
// - Auto-detects if using yarn or npm | |
// - Creates, commits, and pushes the changes in a separate branch | |
import * as Colors from "https://deno.land/std/fmt/colors.ts"; | |
import yargs from "https://deno.land/x/yargs/deno.ts"; | |
import { existsSync } from "https://deno.land/std/fs/mod.ts"; | |
import { iter } from "https://deno.land/std/io/util.ts"; | |
function print(str: string) { | |
console.log(Colors.blue(str)); | |
} | |
interface Arguments { | |
depName: string; | |
depTargetVersion: string; | |
packageManager: string; | |
defaultBranch: string; | |
push: boolean; | |
verbose: boolean; | |
} | |
const argv: Arguments = yargs(Deno.args) | |
.usage( | |
"Bumps the version number of an NPM dependency and commits the changes in a new branch.", | |
) | |
.option("dep-name", { | |
type: "string", | |
description: "Dependency name", | |
demandOption: true, | |
}) | |
.option("dep-target-version", { | |
type: "string", | |
description: "Desired dependency version (defaults to latest)", | |
}) | |
.option("package-manager", { | |
type: "string", | |
description: "'yarn' or 'npm' (defaults to auto-detect)", | |
}) | |
.option("default-branch", { | |
type: "string", | |
description: "Default branch name (defaults to auto-detect)", | |
}) | |
.option("push", { | |
type: "boolean", | |
description: "Push the generated branch?", | |
default: false, | |
}) | |
.option("verbose", { | |
type: "boolean", | |
description: "Show CLI output", | |
default: true, | |
}) | |
.parseSync(); | |
async function run(params: Arguments) { | |
let { | |
depName, | |
depTargetVersion, | |
packageManager, | |
defaultBranch, | |
push, | |
verbose, | |
} = params; | |
const decoder = new TextDecoder(); | |
async function exec(cmd: string) { | |
const p = Deno.run({ | |
cmd: ["/bin/sh", "-c", cmd], | |
stdout: "piped", | |
stderr: "piped", | |
}); | |
let stdout = ""; | |
let stderr = ""; | |
const stdoutPromise = (async function () { | |
for await (const chunk of iter(p.stdout)) { | |
const decoded = decoder.decode(chunk); | |
if (verbose) await Deno.stdout.write(chunk); | |
stdout += decoded; | |
} | |
p.stdout.close(); | |
})(); | |
const stderrPromise = (async function () { | |
for await (const chunk of iter(p.stderr)) { | |
const decoded = decoder.decode(chunk); | |
if (verbose) await Deno.stderr.write(chunk); | |
stderr += decoded; | |
} | |
p.stderr.close(); | |
})(); | |
const { success } = await p.status(); | |
await Promise.all([stdoutPromise, stderrPromise]); | |
p.close(); | |
if (!success) { | |
throw new Error(stderr); | |
} | |
return stdout.trim(); | |
} | |
print( | |
`Bumping "${depName}"${ | |
depTargetVersion ? ` to v${depTargetVersion}` : "" | |
}.`, | |
); | |
if (!packageManager) { | |
packageManager = existsSync("./yarn.lock") ? "yarn" : "npm"; | |
} | |
print(`The project is using ${packageManager}.`); | |
if (!depTargetVersion) { | |
print(`Checking the latest "${depName}" version...`); | |
} | |
depTargetVersion = await exec(`npm show ${depName} version`); | |
print(`Latest version is v${depTargetVersion}`); | |
if (!existsSync("./package.json")) { | |
throw new Error(`./package.json does not exist`); | |
} | |
const packageJson = JSON.parse(await Deno.readTextFile("./package.json")); | |
let dependencyType; | |
let depCurrentVersion; | |
["dependencies", "devDependencies", "peerDependencies"].forEach( | |
(packageDependencyType) => { | |
if (packageJson?.[packageDependencyType]?.[depName]) { | |
dependencyType = packageDependencyType; | |
depCurrentVersion = packageJson?.[packageDependencyType]?.[depName] | |
?.trim(); | |
} | |
}, | |
); | |
if (!dependencyType) { | |
throw new Error( | |
`Couldn't find "${depName}" in package.json`, | |
); | |
} | |
if (depCurrentVersion === depTargetVersion) { | |
print(`"${depName}" in package.json is already at ${depTargetVersion} 👍`); | |
return; | |
} | |
print( | |
`"${depName}" is currenty installed as a ${dependencyType} at version ${depCurrentVersion}`, | |
); | |
if (!defaultBranch) { | |
defaultBranch = await exec( | |
`git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'`, | |
); | |
} | |
print(`Running checkout of the "${defaultBranch}" branch...`); | |
await exec(`git checkout '${defaultBranch}' || exit 1`); | |
await exec(`git pull origin '${defaultBranch}' || exit 1`); | |
const branchName = `bump-${depName}-${depTargetVersion}`; | |
print(`Creating a new branch "${branchName}" branch...`); | |
await exec(`git checkout -b '${branchName}' || exit 1;`); | |
print(`Bumping ${depName}...`); | |
const lib = `${depName}@${depTargetVersion}`; | |
if (packageManager === "yarn") { | |
if (dependencyType === "devDependency") { | |
await exec(`yarn -D add ${lib}`); | |
} else { | |
await exec(`yarn add ${lib}`); | |
} | |
} else { | |
if (dependencyType === "devDependency") { | |
await exec(`npm i -D ${lib}`); | |
} else { | |
await exec(`npm i ${lib}`); | |
} | |
} | |
print("Committing the changes..."); | |
if (packageManager === "yarn") { | |
await exec(`git add package.json yarn.lock`); | |
} else { | |
await exec(`git add package.json package-lock.json`); | |
} | |
const commitTitle = `Bump \`${depName}\` to \`${depTargetVersion}\``; | |
const commitDescription = `Bump \`${depName}\` to \`${depTargetVersion}\``; | |
await exec(`git commit -m '${commitTitle}' -m '${commitDescription}' -n`); | |
if (push) { | |
print("Pushing the changes..."); | |
await exec(`git push origin '${branchName}' --no-verify`); | |
print( | |
`All good 👍, your bump has been pushed to the "${branchName}" branch`, | |
); | |
} else { | |
print(`All good 👍, run "git push origin ${branchName}" when you're ready`); | |
} | |
} | |
try { | |
await run(argv); | |
} catch (err) { | |
console.error(Colors.bold(Colors.red("Error:")), err?.message); | |
} |
You can then pipe | gh pr create
to easily create the Github PR
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage:
Must be run with
--unstable
because its required to runexistsSync
(yes, even if it comes from stdlib)