UNPKG

@alcalzone/release-script

Version:

Release script to automatically increment version numbers and push git tags of Node.js projects

261 lines 8.36 kB
import { exec, execRaw, execute, isReleaseError, ReleaseError, resolvePlugins, stripColors, } from "@alcalzone/release-script-core"; import { distinct } from "alcalzone-shared/arrays"; import enquirer from "enquirer"; import colors from "picocolors"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; const { prompt } = enquirer; function colorizeTextAndTags(textWithTags, textColor, bgColor) { return textColor(textWithTags.replace(/\[(.*?)\]/g, (match, group1) => bgColor("[") + colors.inverse(group1) + bgColor("]"))); } const prefixColors = [ colors.magenta, colors.cyan, colors.yellow, colors.red, colors.green, colors.blue, colors.white, ]; const usedPrefixes = []; function colorizePrefix(prefix) { if (!prefix.includes(":")) return colors.white(prefix); const prefixShort = prefix.split(":").slice(-1)[0]; let prefixIndex = usedPrefixes.indexOf(prefixShort); if (prefixIndex === -1) { usedPrefixes.push(prefixShort); prefixIndex = usedPrefixes.length - 1; } return prefixColors[prefixIndex % prefixColors.length](prefix); } function prependPrefix(prefix, str) { if (!prefix) return str; return colors.bold(colorizePrefix(prefix)) + " " + str; } class CLI { context; constructor(context) { this.context = context; } log(msg) { console.log(prependPrefix(this.context.cli.prefix, msg)); } warn(msg) { console.warn(prependPrefix(this.context.cli.prefix, colorizeTextAndTags(`[WARN] ${msg}`, colors.yellow, colors.bgYellow))); this.context.warnings.push(msg); } error(msg) { console.error(prependPrefix(this.context.cli.prefix, colorizeTextAndTags(`[ERR] ${msg}`, colors.red, colors.bgRed))); this.context.errors.push(msg); } fatal(msg, code) { throw new ReleaseError(msg, true, code); } logCommand(command, args) { if (args?.length) { command += ` ${args.join(" ")}`; } this.log(`$ ${command}`); } clearLines(lines) { if (!process.stdout.isTTY) { return; } process.stdout.moveCursor(0, -lines); process.stdout.clearScreenDown(); } async select(question, options) { try { const result = await prompt({ name: "default", message: question, type: "select", choices: options.map((o) => ({ name: o.value, message: o.label, hint: o.hint ? this.colors.gray(${o.hint}`) : undefined, })), }); return result.default; } catch (e) { // Strg+C if (e === "") this.fatal("Aborted by user"); throw e; } } async ask(question, placeholder) { try { const result = await prompt({ name: "default", message: question, type: "input", initial: placeholder, }); return result.default; } catch (e) { // Strg+C if (e === "") this.fatal("Aborted by user"); throw e; } } prefix = ""; colors = colors; stripColors = stripColors; } export async function main() { const yargsInstance = yargs(hideBin(process.argv)); let argv = yargsInstance .env("RELEASE_SCRIPT") .usage("$0 [<bump> [<preid>]] [options]", "AlCalzone's release script", (yargs) => yargs .positional("bump", { describe: "The version bump to do.", choices: [ "major", "premajor", "minor", "preminor", "patch", "prepatch", "prerelease", ], required: false, }) .positional("preid", { describe: "The prerelease identifier. Only for pre* bumps.", required: false, })) .wrap(yargsInstance.terminalWidth()) // Delay showing help until the second parsing pass .help(false) .alias("v", "version") .options({ config: { alias: "c", describe: "Path to the release config file", config: true, default: ".releaseconfig.json", }, plugins: { alias: "p", describe: "Additional plugins to load", string: true, array: true, }, verbose: { alias: "V", type: "boolean", description: "Enable debug output", default: false, }, yes: { alias: "y", type: "boolean", description: "Answer all (applicable) yes/no prompts with yes", default: false, }, publishAll: { type: "boolean", description: `Bump and publish all non-private packages in monorepos, even if they didn't change`, default: false, }, }); // We do two-pass parsing: // 1. parse the config file and plugins (non-strict) // 2. parse all options (strict) let parsedArgv = (await argv.parseAsync()); const chosenPlugins = distinct([ // These plugins must always be loaded "git", "package", "exec", "version", "changelog", // These are provided by the user ...(parsedArgv.plugins || []), ]); const allPlugins = await Promise.all(chosenPlugins.map(async (plugin) => new (await import(`@alcalzone/release-script-plugin-${plugin}`)).default())); const plugins = resolvePlugins(allPlugins, chosenPlugins); argv = argv .strict() .help(true) .alias("h", "help") .options({ dryRun: { alias: "dry", type: "boolean", description: "Perform a dry-run: check status, describe changes without changing anything", default: false, }, }); // Let plugins hook into the CLI options for (const plugin of plugins) { if (typeof plugin.defineCLIOptions === "function") { argv = plugin.defineCLIOptions(argv); } } parsedArgv = (await argv.parseAsync()); const data = new Map(); const context = { cwd: process.cwd(), cli: undefined, sys: { exec, execRaw, }, argv: parsedArgv, plugins, warnings: [], errors: [], getData: (key) => { if (!data.has(key)) { throw new ReleaseError(`A plugin tried to access non-existent data with key "${key}"`, true); } else { return data.get(key); } }, hasData: (key) => data.has(key), setData: (key, value) => { data.set(key, value); }, }; context.cli = new CLI(context); try { // Initialize plugins for (const plugin of plugins) { await plugin.init?.(context); } // Execute stages await execute(context); const numWarnings = context.warnings.length; const numErrors = context.errors.length; if (numErrors > 0) { let message = `Release did not complete. There ${numErrors + numWarnings !== 1 ? "were" : "was"} ${colors.red(`${numErrors} error${numErrors !== 1 ? "s" : ""}`)}`; if (numWarnings > 0) { message += ` and ${colors.yellow(`${numWarnings} warning${numWarnings !== 1 ? "s" : ""}`)}`; } message += "!"; console.error(); console.error(message); process.exit(1); } } catch (e) { if (isReleaseError(e)) { console.error(prependPrefix(context.cli.prefix, colorizeTextAndTags(`[FATAL] ${e.message.replace("ReleaseError: ", "")}`, colors.red, colors.bgRed))); } else { const msg = e.stack ?? e.message ?? String(e); console.error(prependPrefix(context.cli.prefix, colorizeTextAndTags(`[FATAL] ${msg}`, colors.red, colors.bgRed))); } process.exit(e.code ?? 1); } } void main(); //# sourceMappingURL=index.js.map