import fse from 'fs-extra' import path from 'path' import { ExecSyncOptions, execSync } from 'child_process' import chalk from 'chalk' import prompts from 'prompts' import minimist from 'minimist' import chalkTemplate from 'chalk-template' import { PackageDetails, getPackageDetails, showPackageDetails } from './lib/getPackageDetails' import semver from 'semver' import { updateChangelog } from './utils/updateChangelog' import simpleGit from 'simple-git' const git = simpleGit(path.resolve(__dirname, '..')) const execOpts: ExecSyncOptions = { stdio: 'inherit' } const args = minimist(process.argv.slice(2)) async function main() { const { tag = 'latest', bump = 'patch', pkg } = args if (!semver.RELEASE_TYPES.includes(bump)) { abort(`Invalid bump type: ${bump}.\n\nMust be one of: ${semver.RELEASE_TYPES.join(', ')}`) } if (bump.startsWith('pre') && tag === 'latest') { abort(`Prerelease bumps must have tag: beta or canary`) } const packageDetails = await getPackageDetails(pkg) showPackageDetails(packageDetails) let packagesToRelease: string[] = [] if (packageDetails.length > 1 && !pkg) { ;({ packagesToRelease } = (await prompts({ type: 'multiselect', name: 'packagesToRelease', message: 'Select packages to release', instructions: 'Space to select. Enter to submit.', choices: packageDetails.map((p) => { const title = p?.newCommits ? chalk.bold.green(p?.shortName) : p?.shortName return { title, value: p.shortName, } }), })) as { packagesToRelease: string[] }) if (!packagesToRelease) { abort() } if (packagesToRelease.length === 0) { abort('Please specify a package to publish') } if (packagesToRelease.find((p) => p === 'payload' && packagesToRelease.length > 1)) { abort('Cannot publish payload with other packages. Release Payload first.') } } else { packagesToRelease = [packageDetails[0].shortName] } const packageMap = packageDetails.reduce( (acc, p) => { acc[p.shortName] = p return acc }, {} as Record, ) console.log(chalkTemplate` {bold.green Publishing packages:} {bold.yellow Bump: ${bump}} {bold.yellow Tag: ${tag}} ${packagesToRelease .map((p) => { const { shortName, version } = packageMap[p] return ` ${shortName.padEnd(24)} ${version} -> ${semver.inc(version, bump, tag)}` }) .join('\n')} `) const confirmPublish = await confirm(`Publish ${packagesToRelease.length} package(s)?`) if (!confirmPublish) { abort() } const results: { name: string; success: boolean }[] = [] for (const pkg of packagesToRelease) { const { packagePath, shortName, name: registryName } = packageMap[pkg] try { console.log(chalk.bold(`\n\nšŸš€ Publishing ${shortName}...\n\n`)) let npmVersionCmd = `npm --no-git-tag-version --prefix ${packagePath} version ${bump}` if (tag !== 'latest') { npmVersionCmd += ` --preid ${tag}` } execSync(npmVersionCmd, execOpts) const packageObj = await fse.readJson(`${packagePath}/package.json`) const newVersion = packageObj.version if (pkg === 'payload') { const shouldUpdateChangelog = await confirm(`šŸ§‘ā€šŸ’» Update Changelog?`) if (shouldUpdateChangelog) { updateChangelog({ pkg: packageMap[pkg], bump }) } } const tagName = `${shortName}/${newVersion}` const shouldCommit = await confirm(`šŸ§‘ā€šŸ’» Commit Release?`) if (shouldCommit) { if (pkg === 'payload') { execSync(`git add CHANGELOG.md`, execOpts) } execSync(`git add ${packagePath}/package.json`, execOpts) execSync(`git commit -m "chore(release): ${tagName} [skip ci]" `, execOpts) } const shouldTag = await confirm(`šŸ·ļø Tag ${tagName}?`) if (shouldTag) { execSync(`git tag -a ${tagName} -m "${tagName}"`, execOpts) if (pkg === 'payload') { execSync(`git tag -a v${newVersion} -m "v${newVersion}"`, execOpts) } } let publishCmd = `pnpm publish -C ${packagePath} --no-git-checks` if (tag !== 'latest') { publishCmd += ` --tag ${tag}` } const shouldPublish = await confirm(`🚢 Publish ${registryName}${chalk.yellow('@' + tag)}?`) if (shouldPublish) { execSync(publishCmd, execOpts) } results.push({ name: shortName, success: true }) } catch (error) { console.error(chalk.bold.red(`ERROR: ${error.message}`)) results.push({ name: shortName, success: false }) } } console.log(chalkTemplate` {bold.green Results:} ${results .map(({ name, success }) => ` ${success ? chalk.bold.green('āœ”') : chalk.bold.red('✘')} ${name}`) .join('\n')} `) // Show unpushed commits and tags execSync( `git log --oneline $(git rev-parse --abbrev-ref --symbolic-full-name @{u})..HEAD`, execOpts, ) console.log('\n') const push = await confirm(`Push commits and tags?`) if (push) { console.log(chalk.bold(`\n\nPushing commits and tags...\n\n`)) execSync(`git push --follow-tags`, execOpts) } console.log(chalk.bold.green(`\n\nDone!\n\n`)) } main().catch((error) => { console.error(error) process.exit(1) }) async function abort(message = 'Abort', exitCode = 1) { console.error(chalk.bold.red(`\n${message}\n`)) process.exit(exitCode) } async function confirm(message: string): Promise { const { confirm } = await prompts( { name: 'confirm', initial: false, message, type: 'confirm', }, { onCancel: () => { abort() }, }, ) return confirm }