UNPKG

nhb-scripts

Version:

A collection of Node.js scripts to use in TypeScript & JavaScript projects

265 lines (211 loc) โ€ข 7.03 kB
#!/usr/bin/env node // bin/commit.mjs // @ts-check import { intro, log, outro, select, spinner, text } from '@clack/prompts'; import chalk from 'chalk'; import { execa } from 'execa'; import semver from 'semver'; import { confirm } from '@clack/prompts'; import { isValidArray } from 'nhb-toolbox'; import { mimicClack, normalizeBooleanResult, normalizeStringResult, validateStringInput, } from '../lib/clack-utils.mjs'; import { loadUserConfig } from '../lib/config-loader.mjs'; import { parsePackageJson, writeToPackageJson } from '../lib/package-json-utils.mjs'; import { runFormatter } from '../lib/prettier-formatter.mjs'; /** * * Updates version in package.json * @param {string} newVersion */ async function updateVersion(newVersion) { const pkg = parsePackageJson(); pkg.version = newVersion; await writeToPackageJson(pkg); mimicClack(chalk.green(`โœ“ Version updated to ${chalk.yellowBright(newVersion)}`)); } const bar = chalk.gray('โ”‚'); /** * * Format commit and push `stderr` and `stdout` from `execa` * @param {string} out Output to format. */ function formatStdOut(out) { const msgs = out .split('\n') .filter(Boolean) .map((msg) => chalk.gray(msg.trim())); const bullet = (needBar = true) => chalk.green(`${needBar ? `\n${bar}` : ''} โ€ข `); console.log(bar + bullet(false) + msgs.join(bullet()) + '\n' + bar); } /** * * Git commit and push with message * @param {string} message Commit message * @param {string} version Version string */ export async function commitAndPush(message, version) { const s = spinner(); s.start(chalk.blue('๐Ÿ“ค Changes are committing')); try { await execa('git', ['add', '.']); const { stdout: commitOut } = await execa('git', ['commit', '-m', message]); if (commitOut?.trim()) { log.message('\n'); console.log('๐Ÿ“ค ' + chalk.bold.blue.underline('Commit Summary')); formatStdOut(commitOut); } s.stop(chalk.green('โœ… Changes are committed successfully!')); const shouldPush = normalizeBooleanResult( await confirm({ message: chalk.yellow(`โ” Push to remote repository?`), initialValue: true, }) ); if (shouldPush) { const s2 = spinner(); s2.start(chalk.blue('๐Ÿ“Œ Pushing to remote repository')); const { stdout, stderr } = await execa('git', ['push', '--verbose']); const pushOut = (stdout + '\n' + stderr)?.trim(); if (pushOut) { log.message('\n'); console.log('๐Ÿ“Œ ' + chalk.bold.red.underline('Push Summary')); formatStdOut(pushOut); } s2.stop(chalk.green('โœ… Changes are pushed successfully!')); outro(chalk.green(`๐Ÿš€ Version ${version} pushed with message: "${message}"`)); } else { outro(chalk.green(`๐Ÿš€ Version ${version} committed with message: "${message}"`)); } } catch (err) { s.stop(chalk.red('๐Ÿ›‘ Commit or push failed!')); console.error(chalk.red(err)); process.exit(0); } } /** * @param {string} newVersion * @param {string} currentVersion */ function isValidVersion(newVersion, currentVersion) { if (newVersion === currentVersion) return true; return semver.valid(newVersion) && semver.gte(newVersion, currentVersion); } /** Run the final prompt flow */ async function runCommitPushFlow() { intro(chalk.cyan('๐Ÿš€ Commit & Push')); const pkg = parsePackageJson(); const oldVersion = pkg.version || '0.0.0'; const { runBefore, runAfter, emojiBeforePrefix = false, runFormatter: shouldFormat = false, wrapPrefixWith: wrapPrefixWith = '', commitTypes, } = (await loadUserConfig()).commit ?? {}; mimicClack(`Current version: ${chalk.yellow(oldVersion)}`); let version = ''; while (true) { const input = normalizeStringResult( await text({ message: `${chalk.cyanBright.bold('Enter new version (press enter to skip):')}`, placeholder: oldVersion, defaultValue: oldVersion, initialValue: oldVersion, }) ); version = (input || '').trim(); if (!version) { version = oldVersion; mimicClack(chalk.cyanBright(`๐Ÿ”„๏ธ Using previous version: ${chalk.yellow(version)}`)); break; } if (!isValidVersion(version, oldVersion)) { mimicClack(chalk.red('๐Ÿ›‘ Invalid or older version. Use valid semver like 1.2.3')); continue; } mimicClack(chalk.green(`โœ” Selected version: ${chalk.yellowBright(version)}`)); break; } /** @type {Readonly<import('../types/index.d.ts').CommitType[]>} */ const DEFAULT_CHOICES = Object.freeze([ { emoji: '๐Ÿ”ง', type: 'update' }, { emoji: 'โœจ', type: 'feat' }, { emoji: '๐Ÿ›', type: 'fix' }, { emoji: '๐Ÿ› ๏ธ ', type: 'chore' }, { emoji: '๐Ÿงผ', type: 'refactor' }, { emoji: '๐Ÿงช', type: 'test' }, { emoji: '๐Ÿ“š', type: 'docs' }, { emoji: '๐Ÿ’…', type: 'style' }, { emoji: 'โšก', type: 'perf' }, { emoji: '๐Ÿ”', type: 'revert' }, { emoji: '๐Ÿงฑ', type: 'build' }, { emoji: '๐Ÿš€', type: 'ci' }, { emoji: '๐Ÿ”–', type: 'release' }, { emoji: '๐Ÿ“ฆ', type: 'deps' }, { emoji: '๐Ÿงน', type: 'cleanup' }, { emoji: '๐Ÿงญ', type: 'merge' }, ]); const { custom = [], overrideDefaults = false } = commitTypes || {}; /** @type {Readonly<import('../types/index.d.ts').CommitType[]>} */ const COMBINED = overrideDefaults && isValidArray(custom) ? custom : [...DEFAULT_CHOICES, ...custom]; /** @type {import('@clack/prompts').Option<string>[]} */ const typeChoices = COMBINED.map(({ emoji, type }, idx) => { const tEmoji = emoji.trim(); const tType = type.trim(); return { value: emojiBeforePrefix ? `${tEmoji} ${tType}` : tType, label: `${emoji} ${tType}`, hint: idx === 0 ? 'default' : undefined, }; }); const typeResult = normalizeStringResult( await select({ message: chalk.cyan('Select commit type:'), options: [...typeChoices, { value: '__custom__', label: 'โœ Custom...' }], }) ); let finalType = typeChoices.find((type) => type.value === typeResult)?.value; if (typeResult === '__custom__') { const customType = normalizeStringResult( await text({ message: chalk.magenta('Enter custom commit type:'), validate: validateStringInput, }) ); finalType = customType; } const scopeResult = normalizeStringResult( await text({ message: chalk.gray('Enter a scope (optional):'), placeholder: 'e.g. api, ui, auth', }) ); const messageResult = normalizeStringResult( await text({ message: chalk.cyan('Enter commit message (required):'), placeholder: 'e.g. added new feature, fixed bug in auth module etc.', validate: validateStringInput, }) ); console.log(bar); const formattedMessage = scopeResult ? `${wrapPrefixWith}${finalType}(${scopeResult}):${wrapPrefixWith} ${messageResult}` : `${wrapPrefixWith}${finalType}:${wrapPrefixWith} ${messageResult}`; if (version !== oldVersion) { await updateVersion(version); } runBefore?.(); if (shouldFormat) { await runFormatter(); } await commitAndPush(formattedMessage, version); runAfter?.(); } runCommitPushFlow().catch((err) => { console.error(chalk.red('๐Ÿ›‘ Unexpected Error:'), err); process.exit(0); });