UNPKG

np

Version:
290 lines (257 loc) 9.31 kB
import {execa} from 'execa'; import {deleteAsync} from 'del'; // NOTE: We intentionally use the original `listr` package instead of `listr2`. // listr2's DefaultRenderer uses log-update which has known issues with terminal scrolling // that cause it to overwrite content printed before listr2 started (like our inquirer prompts). // See: https://github.com/cenk1cenk2/listr2/issues/296 import Listr from 'listr'; import { merge, catchError, filter, finalize, from, mergeMap, throwError, } from 'rxjs'; import hostedGitInfo from 'hosted-git-info'; import onetime from 'onetime'; import {asyncExitHook} from 'exit-hook'; import logSymbols from 'log-symbols'; import prerequisiteTasks from './prerequisite-tasks.js'; import gitTasks from './git-tasks.js'; import {getPackagePublishArguments, runPublish} from './npm/publish.js'; import enable2fa, {getEnable2faArguments} from './npm/enable-2fa.js'; import handleNpmError from './npm/handle-npm-error.js'; import {getOidcProvider} from './npm/oidc.js'; import releaseTaskHelper from './release-task-helper.js'; import {findLockfile, printCommand} from './package-manager/index.js'; import * as util from './util.js'; import * as git from './git-util.js'; import * as npm from './npm/util.js'; /** @type {(cmd: string, args: string[], options?: import('execa').Options) => any} */ const exec = (command, arguments_, options) => { // Use `Observable` support if merged https://github.com/sindresorhus/execa/pull/26 const subProcess = execa(command, arguments_, options); return merge(subProcess.stdout, subProcess.stderr, subProcess).pipe( filter(Boolean), catchError(error => { // Include stderr in error message for better diagnostics if (error.stderr) { error.message = `${error.shortMessage}\n${error.stderr}`; } throw error; }), ); }; /** @param {string} input @param {import('./cli-implementation.js').Options} options @param {{package_: import('read-pkg').NormalizedPackageJson; rootDirectory: string}} context */ const np = async (input = 'patch', {packageManager, ...options}, {package_, rootDirectory}) => { // TODO: Remove sometime far in the future if (options.skipCleanup) { options.cleanup = false; } const runTests = options.tests && !options.yolo; const runCleanup = options.cleanup && !options.yolo; const lockfile = findLockfile(rootDirectory, packageManager); const isOnGitHub = options.repoUrl && hostedGitInfo.fromUrl(options.repoUrl)?.type === 'github'; const testScript = options.testScript || 'test'; if (options.releaseDraftOnly) { await releaseTaskHelper(options, package_, packageManager); return package_; } let publishStatus = 'UNKNOWN'; let pushedObjects; const rollback = onetime(async () => { console.log('\nPublish failed. Rolling back to the previous state…'); const tagVersionPrefix = await util.getTagVersionPrefix(packageManager); const latestTag = await git.latestTag(); const versionInLatestTag = latestTag.slice(tagVersionPrefix.length); async function getPackageVersion() { const package_ = await util.readPackage(rootDirectory); return package_.version; } try { // Verify that the package's version has been bumped before deleting the last tag and commit. if (versionInLatestTag === await getPackageVersion() && versionInLatestTag !== package_.version) { await git.deleteTag(latestTag); await git.removeLastCommit(); } console.log('Successfully rolled back the project to its previous state.'); } catch (error) { console.log(`Couldn't roll back because of the following error:\n${error}`); } }); asyncExitHook(async () => { if (options.preview || publishStatus === 'SUCCESS') { return; } if (publishStatus === 'FAILED') { await rollback(); } else { console.log('\nAborted!'); } }, {wait: 2000}); // Don't enable 2FA when using OIDC (Trusted Publishing) as it's already managed const shouldEnable2FA = options['2fa'] && options.availability.isAvailable && !options.availability.isUnknown && !package_.private && !npm.isExternalRegistry(package_) && !getOidcProvider(); // To prevent the process from hanging due to watch mode (e.g. when running `vitest`) const ciEnvOptions = {env: {CI: 'true'}}; /** @param {typeof options} _options */ function getPublishCommand(_options) { const publishCommand = packageManager.publishCommand || (arguments_ => [packageManager.cli, arguments_]); const arguments_ = getPackagePublishArguments(_options); return publishCommand(arguments_); } const tasks = new Listr([ { title: 'Prerequisite check', enabled: () => options.runPublish, task: () => prerequisiteTasks(input, package_, options, {packageManager, rootDirectory}), }, { title: 'Git', task: () => gitTasks(options), }, { title: 'Cleanup', enabled: () => runCleanup && !lockfile, task: () => deleteAsync('node_modules'), }, { title: `Installing dependencies using ${packageManager.id}`, enabled: () => runCleanup, task: () => new Listr([ { title: 'Running install command', task() { const installCommand = lockfile ? packageManager.installCommand : packageManager.installCommandNoLockfile; return exec(...installCommand); }, }, { title: 'Checking working tree is still clean', // If lockfile was out of date and tracked by git, this will fail task: () => git.verifyWorkingTreeIsClean(), }, ]), }, { title: 'Running tests', enabled: () => runTests, task: () => exec(packageManager.cli, ['run', testScript], ciEnvOptions), }, { title: 'Bumping version', skip() { if (options.preview) { const [cli, arguments_] = packageManager.versionCommand(input); if (options.message) { arguments_.push('--message', options.message.replaceAll('%s', input)); } return `[Preview] Command not executed: ${printCommand([cli, arguments_])}`; } }, task() { const [cli, arguments_] = packageManager.versionCommand(input); if (options.message) { arguments_.push('--message', options.message); } // Inherit stdin to allow GPG password prompts for commit signing return exec(cli, arguments_, {stdin: 'inherit'}); }, }, ...options.runPublish ? [ { title: 'Publishing package', skip() { if (options.preview) { const command = getPublishCommand(options); return `[Preview] Command not executed: ${printCommand(command)}.`; } }, /** @type {(context, task) => Listr.ListrTaskResult<any>} */ task(context, task) { let hasError = false; return runPublish(getPublishCommand(options), {cwd: rootDirectory}) .pipe(catchError(error => handleNpmError(error, task, otp => { context.otp = otp; return runPublish(getPublishCommand({...options, otp}), {cwd: rootDirectory}); }))) .pipe( // Note: Cannot use `async` here as the `await` will not finish before the error propagates. catchError(error => { hasError = true; return from(rollback()).pipe( mergeMap(() => throwError(() => new Error(`Error publishing package:\n${error.message}\n\nThe project was rolled back to its previous state.`))), catchError(() => throwError(() => new Error(`Error publishing package:\n${error.message}\n\nThe project was rolled back to its previous state.`))), ); }), finalize(() => { publishStatus = hasError ? 'FAILED' : 'SUCCESS'; }), ); }, }, ...shouldEnable2FA ? [{ title: 'Enabling two-factor authentication', async skip() { if (options.preview) { const arguments_ = await getEnable2faArguments(package_.name, options); return `[Preview] Command not executed: npm ${arguments_.join(' ')}.`; } }, task: (context, task) => enable2fa(task, package_.name, {otp: context.otp}), }] : [], ] : [], { title: 'Pushing tags', async skip() { if (!options.remote && !(await git.hasUpstream())) { return 'Upstream branch not found; not pushing.'; } if (options.preview) { const remote = options.remote ? `${options.remote} ` : ''; return `[Preview] Command not executed: git push ${remote}--follow-tags.`; } if (publishStatus === 'FAILED' && options.runPublish) { return 'Couldn\'t publish package to npm; not pushing.'; } }, async task() { pushedObjects = await git.pushGraceful(isOnGitHub, options.remote); }, }, ...options.releaseDraft ? [{ title: 'Creating release draft on GitHub', enabled: () => isOnGitHub === true, skip() { if (options.preview) { return '[Preview] GitHub Releases draft will not be opened in preview mode.'; } }, task: () => releaseTaskHelper(options, package_, packageManager), }] : [], ], { showSubtasks: false, renderer: options.renderer ?? 'default', clearOutput: !options.preview && !options.releaseDraftOnly, }); if (!options.runPublish) { publishStatus = 'SUCCESS'; } await tasks.run(); if (pushedObjects) { console.error(`\n${logSymbols.error} ${pushedObjects.reason}`); } const {package_: newPackage} = await util.readPackage(); return newPackage; }; export default np;