From 76d0890142b184bec0e1291ab973e1c770d00cc1 Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Fri, 14 Oct 2022 17:17:47 +0800 Subject: [PATCH] Refactor/Fix release script (#13619) --- .eslintignore | 2 + .gitignore | 2 +- .prettierignore | 3 +- package.json | 2 +- scripts/release/README.md | 6 + scripts/release/release.js | 4 +- scripts/release/run.js | 125 ++++++++++-------- scripts/release/steps/generate-bundles.js | 14 +- scripts/release/steps/index.js | 15 +++ scripts/release/steps/install-dependencies.js | 8 +- scripts/release/steps/lint-files.js | 3 + scripts/release/steps/post-publish-steps.js | 44 +++--- scripts/release/steps/publish-to-npm.js | 46 ++++--- scripts/release/steps/push-to-git.js | 12 +- scripts/release/steps/run-tests.js | 8 +- .../show-instructions-after-npm-publish.js | 24 ++-- scripts/release/steps/update-changelog.js | 24 ++-- .../release/steps/update-dependents-count.js | 6 +- scripts/release/steps/update-version.js | 14 +- scripts/release/steps/validate-new-version.js | 10 +- scripts/release/steps/wait-for-bot-release.js | 50 ++++--- .../tests/validate-new-version.test.js | 16 ++- scripts/release/utils.js | 53 +++++--- 23 files changed, 289 insertions(+), 202 deletions(-) create mode 100644 scripts/release/steps/index.js create mode 100644 scripts/release/steps/lint-files.js diff --git a/.eslintignore b/.eslintignore index 13a8b7dc8..180e998cb 100644 --- a/.eslintignore +++ b/.eslintignore @@ -13,3 +13,5 @@ /website/static/playground.js /website/static/lib/ /scripts/benchmark/*/ +**/.yarn/** +**/.pnp.* diff --git a/.gitignore b/.gitignore index d97fb2f5d..81ec75d24 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,7 @@ package-lock.json !.yarn/plugins !.yarn/sdks !.yarn/versions -.pnp.* +**/.pnp.* .nyc_output .devcontainer # When installing software on gitpod.io, `core.*` files are generated diff --git a/.prettierignore b/.prettierignore index 07ca4579a..b8585c085 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,7 +2,8 @@ .cache/ coverage/ .nyc_output/ -.yarn/ +**/.yarn/** +**/.pnp.* /dist*/ node_modules/ diff --git a/package.json b/package.json index c2520c62d..23eba025a 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "author": "James Long", "type": "module", "license": "MIT", - "main": "./index.cjs", + "main": "./src/index.cjs", "browser": "./standalone.js", "unpkg": "./standalone.js", "exports": { diff --git a/scripts/release/README.md b/scripts/release/README.md index d89679b18..d5240a2b2 100644 --- a/scripts/release/README.md +++ b/scripts/release/README.md @@ -8,6 +8,12 @@ node ./scripts/release/release.js --version NEW_VERSION The script has its own `package.json` so we can reinstall the root's `node_modules/` while making the release. +## Flags + +- `--manual` Manual run release process instead of publish from GitHub actions +- `--dry` Dry run +- `--skip-dependencies-install` Skip dependencies installation + ## Credits This script was inspired by [React's release script](https://github.com/facebook/react/tree/001f9ef/scripts/release). diff --git a/scripts/release/release.js b/scripts/release/release.js index 2623a304a..90e650a96 100644 --- a/scripts/release/release.js +++ b/scripts/release/release.js @@ -26,5 +26,5 @@ await runCommand("git fetch --tags"); // Install script's dependencies before any require await runCommand("yarn"); -const { default: run } = await import("./run.js"); -await run(); +// Ready to run +await import("./run.js"); diff --git a/scripts/release/run.js b/scripts/release/run.js index 55f163e61..71c663ade 100644 --- a/scripts/release/run.js +++ b/scripts/release/run.js @@ -1,61 +1,76 @@ -import chalk from "chalk"; import semver from "semver"; -import { outdent } from "outdent"; import { runGit, readJson } from "./utils.js"; import parseArguments from "./parse-arguments.js"; +import { logPromise } from "./utils.js"; +import * as steps from "./steps/index.js"; -const importDefault = async (module) => (await import(module)).default; - -async function run() { - const params = parseArguments(); - - const { stdout: previousVersion } = await runGit([ - "describe", - "--tags", - "--abbrev=0", - ]); - - if (semver.parse(previousVersion) === null) { - throw new Error(`Unexpected previousVersion: ${previousVersion}`); - } else { - params.previousVersion = previousVersion; - params.previousVersionOnDefaultBranch = ( - await readJson("package.json") - ).version; - } - - const steps = await Promise.all( - [ - "./steps/validate-new-version.js", - "./steps/check-git-status.js", - !params.skipDependenciesInstall && "./steps/install-dependencies.js", - params.manual && "./steps/run-tests.js", - "./steps/update-version.js", - params.manual && "./steps/generate-bundles.js", - "./steps/update-changelog.js", - "./steps/push-to-git.js", - params.manual - ? "./steps/publish-to-npm.js" - : "./steps/wait-for-bot-release.js", - "./steps/show-instructions-after-npm-publish.js", - "./steps/update-dependents-count.js", - "./steps/bump-prettier.js", - "./steps/post-publish-steps.js", - ] - .filter(Boolean) - .map((step) => importDefault(step)) - ); - - try { - for (const step of steps) { - await step(params); - } - } catch (error) { - const message = outdent.string(error.message.trim()); - const stack = error.stack.replace(message, ""); - console.error(`${chalk.red("error")} ${message}\n${stack}`); - process.exitCode = 1; - } +const params = parseArguments(); +const { stdout: previousVersion } = await runGit([ + "describe", + "--tags", + "--abbrev=0", +]); +if (semver.parse(previousVersion) === null) { + throw new Error(`Unexpected previousVersion: ${previousVersion}`); +} else { + params.previousVersion = previousVersion; + params.previousVersionOnDefaultBranch = ( + await readJson("package.json") + ).version; } -export default run; +for (let step of [ + { + name: "Validating new version", + process: steps.validateNewVersion, + }, + { + name: "Checking git status", + process: steps.checkGitStatus, + skip: params.dry, + }, + { + name: "Installing NPM dependencies", + process: steps.installDependencies, + skip: params.dry || params.skipDependenciesInstall || !params.manual, + }, + { + name: "Linting files", + process: steps.lintFiles, + skip: params.dry, + }, + { + name: "Running tests", + process: steps.runTests, + skip: params.dry || !params.manual, + }, + { + name: "Bumping version", + process: steps.updateVersion, + skip: params.dry, + }, + steps.generateBundles, + steps.updateChangelog, + { + name: "Committing and pushing to remote", + process: steps.pushToGit, + skip: params.dry, + }, + params.manual ? steps.publishToNpm : steps.waitForBotRelease, + steps.showInstructionsAfterNpmPublish, + steps.updateDependentsCount, + steps.bumpPrettier, + steps.postPublishSteps, +]) { + if (typeof step === "function") { + step = { process: step }; + } + + const runStep = () => step.process(params); + + if (step.name) { + await logPromise(step.name, runStep, step.skip); + } else if (!step.skip) { + await runStep(); + } +} diff --git a/scripts/release/steps/generate-bundles.js b/scripts/release/steps/generate-bundles.js index 1a36232aa..9de180cd5 100644 --- a/scripts/release/steps/generate-bundles.js +++ b/scripts/release/steps/generate-bundles.js @@ -1,20 +1,28 @@ import chalk from "chalk"; import { runYarn, logPromise, readJson } from "../utils.js"; -export default async function generateBundles({ version }) { +export default async function generateBundles({ dry, version, manual }) { + if (!manual) { + return; + } + await logPromise( "Generating bundles", runYarn(["build", "--clean", "--print-size", "--compare-size"]) ); const builtPkg = await readJson("dist/package.json"); - if (builtPkg.version !== version) { + if (!dry && builtPkg.version !== version) { throw new Error( `Expected ${version} in dist/package.json but found ${builtPkg.version}` ); } - await logPromise("Running tests on generated bundles", runYarn("test:dist")); + await logPromise( + "Running tests on generated bundles", + () => runYarn("test:dist"), + /* shouldSkip */ dry + ); console.log(chalk.green.bold("Build successful!\n")); } diff --git a/scripts/release/steps/index.js b/scripts/release/steps/index.js new file mode 100644 index 000000000..dd381281d --- /dev/null +++ b/scripts/release/steps/index.js @@ -0,0 +1,15 @@ +export { default as validateNewVersion } from "./validate-new-version.js"; +export { default as checkGitStatus } from "./check-git-status.js"; +export { default as installDependencies } from "./install-dependencies.js"; +export { default as lintFiles } from "./lint-files.js"; +export { default as runTests } from "./run-tests.js"; +export { default as updateVersion } from "./update-version.js"; +export { default as generateBundles } from "./generate-bundles.js"; +export { default as updateChangelog } from "./update-changelog.js"; +export { default as pushToGit } from "./push-to-git.js"; +export { default as publishToNpm } from "./publish-to-npm.js"; +export { default as waitForBotRelease } from "./wait-for-bot-release.js"; +export { default as showInstructionsAfterNpmPublish } from "./show-instructions-after-npm-publish.js"; +export { default as updateDependentsCount } from "./update-dependents-count.js"; +export { default as bumpPrettier } from "./bump-prettier.js"; +export { default as postPublishSteps } from "./post-publish-steps.js"; diff --git a/scripts/release/steps/install-dependencies.js b/scripts/release/steps/install-dependencies.js index 106d879d9..78f890cb9 100644 --- a/scripts/release/steps/install-dependencies.js +++ b/scripts/release/steps/install-dependencies.js @@ -1,7 +1,7 @@ import { execa } from "execa"; -import { runYarn, runGit, logPromise } from "../utils.js"; +import { runYarn, runGit } from "../utils.js"; -async function install() { +export default async function installDependencies() { await execa("rm", ["-rf", "node_modules"]); await runYarn(["install"]); @@ -15,7 +15,3 @@ async function install() { ); } } - -export default function installDependencies() { - return logPromise("Installing NPM dependencies", install()); -} diff --git a/scripts/release/steps/lint-files.js b/scripts/release/steps/lint-files.js new file mode 100644 index 000000000..b92c69c21 --- /dev/null +++ b/scripts/release/steps/lint-files.js @@ -0,0 +1,3 @@ +import { runYarn } from "../utils.js"; + +export default () => runYarn("lint"); diff --git a/scripts/release/steps/post-publish-steps.js b/scripts/release/steps/post-publish-steps.js index 6a3accc2d..c59aff289 100644 --- a/scripts/release/steps/post-publish-steps.js +++ b/scripts/release/steps/post-publish-steps.js @@ -3,8 +3,6 @@ import outdent from "outdent"; import { execa } from "execa"; import { fetchText, logPromise } from "../utils.js"; -const outdentString = outdent.string; - const SCHEMA_REPO = "SchemaStore/schemastore"; const SCHEMA_PATH = "src/schemas/json/prettierrc.json"; const RAW_URL = `https://raw.githubusercontent.com/${SCHEMA_REPO}/master/${SCHEMA_PATH}`; @@ -25,40 +23,46 @@ async function checkSchema() { return; } - return outdentString(chalk` - {bold.underline The schema in {yellow SchemaStore} needs an update.} - - Open {cyan.underline ${EDIT_URL}} - - Run {yellow node scripts/generate-schema.mjs} and copy the new schema + return outdent` + ${chalk.bold.underline( + "The schema in {yellow SchemaStore" + )} needs an update.} + - Open ${chalk.cyan.underline(EDIT_URL)} + - Run ${chalk.yellow( + "node scripts/generate-schema.mjs" + )} and copy the new schema - Paste it on GitHub interface - Open a PR - `); + `; } function twitterAnnouncement() { - return outdentString(chalk` - {bold.underline Announce on Twitter} - - Open {cyan.underline https://tweetdeck.twitter.com} + return outdent` + ${chalk.bold.underline("Announce on Twitter")} + - Open ${chalk.cyan.underline("https://tweetdeck.twitter.com")} - Make sure you are tweeting from the {yellow @PrettierCode} account. - Tweet about the release, including the blog post URL. - `); + `; } -export default async function postPublishSteps() { - const steps = [await checkSchema(), twitterAnnouncement()].filter(Boolean); - +export default async function postPublishSteps({ dry }) { console.log(chalk.bold.green("The script has finished!\n")); - if (steps.length === 0) { + if (dry) { return; } + const steps = [await checkSchema(), twitterAnnouncement()].filter(Boolean); + console.log( - outdentString(chalk` - {yellow.bold The following ${ - steps.length === 1 ? "step is" : "steps are" - } optional.} + outdent` + ${chalk.yellow.bold( + `The following ${ + steps.length === 1 ? "step is" : "steps are" + } optional.` + )} ${steps.join("\n\n")} - `) + ` ); } diff --git a/scripts/release/steps/publish-to-npm.js b/scripts/release/steps/publish-to-npm.js index 775c0e439..3ee0a8ae2 100644 --- a/scripts/release/steps/publish-to-npm.js +++ b/scripts/release/steps/publish-to-npm.js @@ -1,19 +1,35 @@ import { execa } from "execa"; import enquirer from "enquirer"; -import { logPromise } from "../utils.js"; +import { waitForEnter } from "../utils.js"; + +export default async function publishToNpm({ dry }) { + console.log(`Ready to publish to NPM${dry ? "(--dry-run)" : ""}`); + + await waitForEnter(); + + const commonArgs = ["publish"]; + if (dry) { + commonArgs.push("--dry-run"); + } -/** - * Retry "npm publish" when to enter OTP is failed. - */ -async function retryNpmPublish() { const runNpmPublish = async () => { - const { otp } = await enquirer.prompt({ - type: "input", - name: "otp", - message: "Please enter your npm OTP", - }); - await execa("npm", ["publish", "--otp", otp], { cwd: "./dist" }); + const args = [...commonArgs]; + + if (!dry) { + const { otp } = await enquirer.prompt({ + type: "input", + name: "otp", + message: "Please enter your npm OTP", + }); + args.push("--otp", otp); + } + + await execa("npm", args, { cwd: "./dist" }); }; + + /** + * Retry "npm publish" when to enter OTP is failed. + */ for (let i = 5; i > 0; i--) { try { return await runNpmPublish(); @@ -26,11 +42,3 @@ async function retryNpmPublish() { } } } - -export default async function publishToNpm({ dry }) { - if (dry) { - return; - } - - await logPromise("Publishing to npm", retryNpmPublish()); -} diff --git a/scripts/release/steps/push-to-git.js b/scripts/release/steps/push-to-git.js index 9fd548d3c..7304c56ac 100644 --- a/scripts/release/steps/push-to-git.js +++ b/scripts/release/steps/push-to-git.js @@ -1,16 +1,8 @@ -import { runGit, logPromise } from "../utils.js"; +import { runGit } from "../utils.js"; -async function pushGit({ version, repo }) { +export default async function pushToGit({ version, repo }) { await runGit(["commit", "-am", `Release ${version}`]); await runGit(["tag", "-a", version, "-m", `Release ${version}`]); await runGit(["push", "--repo", repo]); await runGit(["push", "--tags", "--repo", repo]); } - -export default function pushToGit(params) { - if (params.dry) { - return; - } - - return logPromise("Committing and pushing to remote", pushGit(params)); -} diff --git a/scripts/release/steps/run-tests.js b/scripts/release/steps/run-tests.js index 268a104bf..3786928bd 100644 --- a/scripts/release/steps/run-tests.js +++ b/scripts/release/steps/run-tests.js @@ -1,7 +1,3 @@ -import { runYarn, logPromise } from "../utils.js"; +import { runYarn } from "../utils.js"; -export default async function runTests() { - await logPromise("Running linter", runYarn("lint:eslint")); - await logPromise("Running Prettier on docs", runYarn("lint:prettier")); - await logPromise("Running tests", runYarn("test")); -} +export default () => runYarn("test"); diff --git a/scripts/release/steps/show-instructions-after-npm-publish.js b/scripts/release/steps/show-instructions-after-npm-publish.js index d63cd2ee9..944ac2115 100644 --- a/scripts/release/steps/show-instructions-after-npm-publish.js +++ b/scripts/release/steps/show-instructions-after-npm-publish.js @@ -7,8 +7,6 @@ import { waitForEnter, } from "../utils.js"; -const outdentString = outdent.string; - const RELEASE_URL_BASE = "https://github.com/prettier/prettier/releases/new?"; export function getReleaseUrl(version, previousVersion) { const semverDiff = semver.diff(version, previousVersion); @@ -42,22 +40,24 @@ export default async function showInstructionsAfterNpmPublish({ const releaseUrl = getReleaseUrl(version, previousVersion); console.log( - outdentString(chalk` - {green.bold Prettier ${version} published!} + outdent` + ${chalk.green.bold(`Prettier ${version} published!`)} - {yellow.bold Some manual steps are necessary.} + ${chalk.yellow.bold("Some manual steps are necessary.")} - {bold.underline Create a GitHub Release} - - Go to {cyan.underline ${releaseUrl}} - - Press {bgGreen.black Publish release } + ${chalk.bold.underline("Create a GitHub Release")} + - Go to ${chalk.cyan.underline(releaseUrl)} + - Press ${chalk.bgGreen.black("Publish release ")} - {bold.underline Test the new release} - - In a new session, run {yellow npm i prettier@latest} in another directory + ${chalk.bold.underline("Test the new release")} + - In a new session, run ${chalk.yellow( + "npm i prettier@latest" + )} in another directory - Test the API and CLI After that, we can proceed to bump this repo's Prettier dependency. - Press ENTER to continue. - `) + ` ); + await waitForEnter(); } diff --git a/scripts/release/steps/update-changelog.js b/scripts/release/steps/update-changelog.js index c49de6760..b18d18fa9 100644 --- a/scripts/release/steps/update-changelog.js +++ b/scripts/release/steps/update-changelog.js @@ -1,7 +1,6 @@ import fs from "node:fs"; import { execa } from "execa"; import chalk from "chalk"; -import outdent from "outdent"; import semver from "semver"; import { waitForEnter, @@ -11,8 +10,6 @@ import { getChangelogContent, } from "../utils.js"; -const outdentString = outdent.string; - function writeChangelog(params) { const changelog = fs.readFileSync("CHANGELOG.md", "utf8"); const newEntry = `# ${params.version}\n\n` + getChangelogContent(params); @@ -30,7 +27,15 @@ async function getChangelogForPatch({ version, previousVersion }) { return changelog; } -export default async function updateChangelog({ version, previousVersion }) { +export default async function updateChangelog({ + dry, + version, + previousVersion, +}) { + if (dry) { + return; + } + const semverDiff = semver.diff(version, previousVersion); if (semverDiff !== "patch") { @@ -45,11 +50,11 @@ export default async function updateChangelog({ version, previousVersion }) { return; } console.warn( - outdentString(chalk` - {yellow warning} The file {bold ${blogPost.file}} doesn't exist, but it will be referenced in {bold CHANGELOG.md}. Make sure to create it later. - - Press ENTER to continue. - `) + `${chalk.yellow("warning")} The file ${chalk.bold( + blogPost.file + )} doesn't exist, but it will be referenced in ${chalk.bold( + "CHANGELOG.md" + )}. Make sure to create it later.` ); } else { const body = await getChangelogForPatch({ @@ -61,7 +66,6 @@ export default async function updateChangelog({ version, previousVersion }) { previousVersion, body, }); - console.log("Press ENTER to continue."); } await waitForEnter(); diff --git a/scripts/release/steps/update-dependents-count.js b/scripts/release/steps/update-dependents-count.js index 1d402ed15..4f2c470f1 100644 --- a/scripts/release/steps/update-dependents-count.js +++ b/scripts/release/steps/update-dependents-count.js @@ -71,7 +71,11 @@ function formatNumber(value) { return Math.floor(value / 1e5) / 10 + " million"; } -export default async function updateDependentsCount() { +export default async function updateDependentsCount({ dry }) { + if (dry) { + return; + } + try { await update(); } catch (error) { diff --git a/scripts/release/steps/update-version.js b/scripts/release/steps/update-version.js index 56ab17054..5f937509b 100644 --- a/scripts/release/steps/update-version.js +++ b/scripts/release/steps/update-version.js @@ -1,12 +1,6 @@ -import { - runYarn, - logPromise, - readJson, - writeJson, - processFile, -} from "../utils.js"; +import { runYarn, readJson, writeJson, processFile } from "../utils.js"; -async function bump({ version }) { +export default async function updateVersion({ version }) { const pkg = await readJson("package.json"); pkg.version = version; await writeJson("package.json", pkg); @@ -34,7 +28,3 @@ async function bump({ version }) { cwd: "./website", }); } - -export default async function updateVersion(params) { - await logPromise("Bumping version", bump(params)); -} diff --git a/scripts/release/steps/validate-new-version.js b/scripts/release/steps/validate-new-version.js index 09ed33051..ccc2a86e8 100644 --- a/scripts/release/steps/validate-new-version.js +++ b/scripts/release/steps/validate-new-version.js @@ -2,13 +2,19 @@ import chalk from "chalk"; import semver from "semver"; export default function validateNewVersion({ version, previousVersion }) { + if (!version) { + throw new Error("'--version' is required"); + } + if (!semver.valid(version)) { - throw new Error("Invalid version specified"); + throw new Error( + `Invalid version '${chalk.red.underline(version)}' specified` + ); } if (!semver.gt(version, previousVersion)) { throw new Error( - `Version ${chalk.yellow(version)} has already been published` + `Version '${chalk.yellow.underline(version)}' has already been published` ); } } diff --git a/scripts/release/steps/wait-for-bot-release.js b/scripts/release/steps/wait-for-bot-release.js index ed01efd81..87a51599a 100644 --- a/scripts/release/steps/wait-for-bot-release.js +++ b/scripts/release/steps/wait-for-bot-release.js @@ -2,8 +2,6 @@ import chalk from "chalk"; import outdent from "outdent"; import { waitForEnter, logPromise } from "../utils.js"; -const outdentString = outdent.string; - export async function isVersionReleased(version) { const response = await fetch("https://registry.npmjs.org/prettier/"); const result = await response.json(); @@ -23,8 +21,6 @@ async function checkBotPermission() { return maintainers.some(({ name }) => name === "prettier-bot"); } -checkBotPermission(); - const sleep = () => new Promise((resolve) => { setTimeout(resolve, 30_000); @@ -37,35 +33,45 @@ export default async function waitForBotRelease({ dry, version }) { if (!(await checkBotPermission())) { console.log( - outdentString(chalk/* indent */ ` - 1. Go to {green.underline https://www.npmjs.com/package/prettier/access} - 2. Add "{yellow prettier-bot}" as prettier package maintainer. - - Press ENTER to continue. - `) + outdent` + 1. Go to ${chalk.green.underline( + "https://www.npmjs.com/package/prettier/access" + )} + 2. Add "${chalk.yellow("prettier-bot")}" as prettier package maintainer. + ` ); await waitForEnter(); } console.log( - outdentString(chalk/* indent */ ` - 1. Go to {green.underline https://www.npmjs.com/package/prettier/access} - 2. Make sure "{yellow Publishing access}" section is set to "{yellow Require two-factor authentication or automation tokens}". - - Press ENTER to continue. - `) + outdent` + 1. Go to ${chalk.green.underline( + "https://www.npmjs.com/package/prettier/access" + )} + 2. Make sure "${chalk.yellow( + "Publishing access" + )}" section is set to "${chalk.yellow( + "Require two-factor authentication or automation tokens" + )}". + ` ); await waitForEnter(); console.log( - outdentString(chalk/* indent */ ` - 1. Go to {green.underline https://github.com/prettier/release-workflow/actions/workflows/release.yml} - 2. Click "{green Run workflow}" button, type "{yellow.underline ${version}}" in "Version to release", uncheck all checkboxes, hit the "{bgGreen Run workflow}" button. - - Press ENTER to continue. - `) + outdent` + 1. Go to ${chalk.green.underline( + "https://github.com/prettier/release-workflow/actions/workflows/release.yml" + )} + 2. Click "${chalk.green( + "Run workflow" + )}" button, type "${chalk.yellow.underline( + version + )}" in "Version to release", uncheck all checkboxes, hit the "${chalk.bgGreen( + "Run workflow" + )}" button. + ` ); await waitForEnter(); diff --git a/scripts/release/tests/validate-new-version.test.js b/scripts/release/tests/validate-new-version.test.js index 2f0f59d3c..22dc6df47 100644 --- a/scripts/release/tests/validate-new-version.test.js +++ b/scripts/release/tests/validate-new-version.test.js @@ -4,12 +4,20 @@ import chalk from "chalk"; import validateNewVersion from "../steps/validate-new-version.js"; describe("validate-new-version", () => { + it("throws error for missing version", () => { + assert.throws( + () => { + validateNewVersion({}); + }, + { message: "'--version' is required" } + ); + }); it("throws error for invalid semver", () => { assert.throws( () => { validateNewVersion({ version: "foo" }); }, - { message: "Invalid version specified" } + { message: `Invalid version '${chalk.red.underline("foo")}' specified` } ); }); it("throws error when version isn't greater than prev version", () => { @@ -17,7 +25,11 @@ describe("validate-new-version", () => { () => { validateNewVersion({ version: "0.0.1", previousVersion: "0.0.2" }); }, - { message: `Version ${chalk.yellow("0.0.1")} has already been published` } + { + message: `Version '${chalk.yellow( + "0.0.1" + )}' has already been published`, + } ); }); }); diff --git a/scripts/release/utils.js b/scripts/release/utils.js index a7e5263ec..8dfc470ee 100644 --- a/scripts/release/utils.js +++ b/scripts/release/utils.js @@ -2,38 +2,54 @@ import fs from "node:fs"; import readline from "node:readline"; import chalk from "chalk"; import { execa } from "execa"; -import stringWidth from "string-width"; import outdent from "outdent"; import getFormattedDate from "./get-formatted-date.js"; readline.emitKeypressEvents(process.stdin); -const OK = chalk.bgGreen.black(" DONE "); -const FAIL = chalk.bgRed.black(" FAIL "); +const statusConfig = [ + { color: "bgGreen", text: "DONE" }, + { color: "bgRed", text: "FAIL" }, + { color: "bgGray", text: "SKIPPED" }, +]; +const maxLength = Math.max(...statusConfig.map(({ text }) => text.length)) + 2; +const padStatusText = (text) => { + while (text.length < maxLength) { + text = text.length % 2 ? `${text} ` : ` ${text}`; + } + return text; +}; +const status = {}; +for (const { color, text } of statusConfig) { + status[text] = chalk[color].black(padStatusText(text)); +} -function fitTerminal(input) { - const columns = Math.min(process.stdout.columns, 80); - const WIDTH = columns - stringWidth(OK) + 1; +function fitTerminal(input, suffix = "") { + const columns = Math.min(process.stdout.columns || 40, 80); + const WIDTH = columns - maxLength + 1; if (input.length < WIDTH) { - input += chalk.dim(".").repeat(WIDTH - input.length - 1); + const repeatCount = Math.max(WIDTH - input.length - 1 - suffix.length, 0); + input += chalk.dim(".").repeat(repeatCount) + suffix; } return input; } -async function logPromise(name, promiseOrAsyncFunction) { - const promise = - typeof promiseOrAsyncFunction === "function" - ? promiseOrAsyncFunction() - : promiseOrAsyncFunction; - +async function logPromise(name, promiseOrAsyncFunction, shouldSkip = false) { process.stdout.write(fitTerminal(name)); + if (shouldSkip) { + process.stdout.write(`${status.SKIPPED}\n`); + return; + } + try { - const result = await promise; - process.stdout.write(`${OK}\n`); + const result = await (typeof promiseOrAsyncFunction === "function" + ? promiseOrAsyncFunction() + : promiseOrAsyncFunction); + process.stdout.write(`${status.DONE}\n`); return result; } catch (error) { - process.stdout.write(`${FAIL}\n`); + process.stdout.write(`${status.FAIL}\n`); throw error; } } @@ -42,7 +58,7 @@ async function runYarn(args, options) { args = Array.isArray(args) ? args : [args]; try { - return await execa("yarn", ["--silent", ...args], options); + return await execa("yarn", ["--silent", "run", ...args], options); } catch (error) { throw new Error(`\`yarn ${args.join(" ")}\` failed\n${error.stdout}`); } @@ -54,6 +70,9 @@ function runGit(args, options) { } function waitForEnter() { + console.log(); + console.log(chalk.gray("Press ENTER to continue.")); + process.stdin.setRawMode(true); return new Promise((resolve, reject) => {